diff --git a/cypress/e2e/product-list.cy.ts b/cypress/e2e/product-list.cy.ts index 0f9cb364d0..a7eda724eb 100644 --- a/cypress/e2e/product-list.cy.ts +++ b/cypress/e2e/product-list.cy.ts @@ -1,5 +1,8 @@ import { TEST_URL } from './constant'; +describe('개별 상품 장바구니 추가 테스트', () => { +import { TEST_URL } from './constant'; + describe('개별 상품 장바구니 추가 테스트', () => { beforeEach(() => { cy.visit(TEST_URL); @@ -20,49 +23,58 @@ describe('개별 상품 장바구니 추가 테스트', () => { }); it('장바구니 수량 카운터에 영어는 입력되지 않는다.', () => { - cy.typingFirstProduct('nave', ''); + cy.typingFirstProduct('nave', '0'); }); it('장바구니 수량 카운터에 한국어는 입력되지 않는다.', () => { - cy.typingFirstProduct('네이브', ''); + cy.typingFirstProduct('네이브', '0'); }); it('장바구니 수량 카운터에 숫자 기호도 입력되지 않는다.', () => { - cy.typingFirstProduct('-', ''); - cy.typingFirstProduct('+', ''); - cy.typingFirstProduct('e', ''); - }); - - it('장바구니 수량 카운터에 아무것도 입력되지않고 blur 되면 기본 값으로 설정한다.', () => { - cy.get('@firstProductItem').find('input').clear().blur(); - cy.get('@firstProductItem').find('input').should('have.value', '1'); + cy.typingFirstProduct('-', '0'); + cy.typingFirstProduct('+', '0'); + cy.typingFirstProduct('e', '0'); }); }); +describe('장바구니 수량 변경 테스트', () => { describe('장바구니 수량 변경 테스트', () => { beforeEach(() => { cy.visit(TEST_URL); + cy.visit(TEST_URL); }); + it('상품을 장바구니에 추가하면 수량이 증가한다.', () => { + cy.get('ul').find('li').first().as('firstProductItem'); it('상품을 장바구니에 추가하면 수량이 증가한다.', () => { cy.get('ul').find('li').first().as('firstProductItem'); + cy.get('@firstProductItem').find('button').click(); cy.get('@firstProductItem').find('button').click(); + cy.get('@firstProductItem').find('button').last().click(); cy.get('@firstProductItem').find('button').last().click(); cy.get('header').find('.sc-gueYoa').should('have.text', '1'); + cy.get('header').find('.sc-gueYoa').should('have.text', '1'); }); + it('이미 등록된 상품을 추가하면 수량은 변경되지 않는다.', () => { + cy.get('ul').find('li').first().as('firstProductItem'); it('이미 등록된 상품을 추가하면 수량은 변경되지 않는다.', () => { cy.get('ul').find('li').first().as('firstProductItem'); + cy.get('@firstProductItem').find('button').click(); + cy.get('@firstProductItem').find('button').last().click(); cy.get('@firstProductItem').find('button').click(); cy.get('@firstProductItem').find('button').last().click(); + cy.get('@firstProductItem').find('button').click(); + cy.get('@firstProductItem').find('button').last().click(); cy.get('@firstProductItem').find('button').click(); cy.get('@firstProductItem').find('button').last().click(); cy.get('header').find('.sc-gueYoa').should('have.text', '1'); + cy.get('header').find('.sc-gueYoa').should('have.text', '1'); }); }); diff --git a/package-lock.json b/package-lock.json index 6825c9fc32..52c43bb7a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ }, "devDependencies": { "@types/styled-components": "^5.1.26", - "cypress": "^12.12.0" + "cypress": "^12.12.0", + "msw": "^1.2.1" } }, "node_modules/@adobe/css-tools": { @@ -3121,6 +3122,47 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "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/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -3181,6 +3223,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", @@ -3933,6 +3981,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.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -4039,6 +4102,12 @@ "pretty-format": "^27.0.0" } }, + "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/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -4054,6 +4123,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "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": "16.18.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", @@ -4156,6 +4231,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/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -4585,6 +4669,15 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz", + "integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==", + "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", @@ -4595,6 +4688,13 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "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", @@ -5486,6 +5586,17 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -5815,6 +5926,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/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -5933,6 +6050,18 @@ "node": ">=8" } }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-table3": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", @@ -5964,6 +6093,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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", @@ -5974,6 +6112,15 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6975,6 +7122,18 @@ "node": ">= 10" } }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -8379,6 +8538,44 @@ "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/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor/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/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -9210,6 +9407,15 @@ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, + "node_modules/graphql": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", + "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -9321,6 +9527,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", @@ -9725,6 +9937,102 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "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", @@ -9902,6 +10210,21 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -9929,6 +10252,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -9953,6 +10285,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", @@ -12251,6 +12589,15 @@ "jiti": "bin/jiti.js" } }, + "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-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -13064,27 +13411,217 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "node_modules/msw": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", + "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "dev": true, + "hasInstallScript": true, "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" + "@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": { - "multicast-dns": "cli.js" + "msw": "cli/index.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.4.x <= 5.0.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "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": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } + "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", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } }, "node_modules/nanoid": { "version": "3.3.6", @@ -13135,6 +13672,48 @@ "tslib": "^2.0.3" } }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -13424,12 +14003,120 @@ "node": ">= 0.8.0" } }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/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/ora/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/ora/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/ora/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/ora/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/ora/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/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/ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", "dev": true }, + "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": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -15890,6 +16577,15 @@ "node": ">=8" } }, + "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", @@ -16237,6 +16933,12 @@ "node": ">= 0.8.0" } }, + "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", @@ -16533,6 +17235,12 @@ "node": ">= 0.4" } }, + "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", @@ -17557,6 +18265,19 @@ "requires-port": "^1.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -17686,6 +18407,27 @@ "minimalistic-assert": "^1.0.0" } }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "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/web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", @@ -20699,6 +21441,43 @@ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==" }, + "@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" + } + } + } + }, "@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -20746,6 +21525,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", @@ -21253,6 +22038,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.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", @@ -21359,6 +22159,12 @@ "pretty-format": "^27.0.0" } }, + "@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/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -21374,6 +22180,12 @@ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" }, + "@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": "16.18.27", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.27.tgz", @@ -21476,6 +22288,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/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -21802,6 +22623,12 @@ "@xtuc/long": "4.2.2" } }, + "@xmldom/xmldom": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz", + "integrity": "sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg==", + "dev": true + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -21812,6 +22639,13 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "@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", @@ -22453,6 +23287,17 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==" }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "blob-util": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", @@ -22685,6 +23530,12 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==" }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, "check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -22766,6 +23617,12 @@ "restore-cursor": "^3.1.0" } }, + "cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true + }, "cli-table3": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", @@ -22786,6 +23643,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", @@ -22796,6 +23659,12 @@ "wrap-ansi": "^7.0.0" } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -23533,6 +24402,15 @@ "execa": "^5.0.0" } }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, "define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -24579,6 +25457,37 @@ "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" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "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" + } + } + } + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -25176,6 +26085,12 @@ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, + "graphql": { + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.6.0.tgz", + "integrity": "sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==", + "dev": true + }, "gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -25248,6 +26163,12 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "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", @@ -25501,49 +26422,123 @@ "dependencies": { "resolve-from": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + } + } + }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "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" + } } } }, - "import-local": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" - }, "internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -25658,6 +26653,15 @@ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -25676,6 +26680,12 @@ "is-path-inside": "^3.0.2" } }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, "is-map": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", @@ -25691,6 +26701,12 @@ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==" }, + "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", @@ -27353,6 +28369,12 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==" }, + "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-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -27957,6 +28979,134 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "msw": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", + "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "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 + } + } + }, "multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", @@ -27966,6 +29116,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 + }, "mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -28010,6 +29166,39 @@ "tslib": "^2.0.3" } }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, "node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -28209,12 +29398,92 @@ "word-wrap": "^1.2.3" } }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.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.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" + } + } + } + }, + "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 + }, "ospath": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", "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": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -29812,6 +31081,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", @@ -30062,6 +31337,12 @@ "send": "0.18.0" } }, + "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", @@ -30293,6 +31574,12 @@ "internal-slot": "^1.0.4" } }, + "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", @@ -31037,6 +32324,19 @@ "requires-port": "^1.0.0" } }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -31143,6 +32443,25 @@ "minimalistic-assert": "^1.0.0" } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "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" + } + }, "web-vitals": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz", diff --git a/package.json b/package.json index 1552653dcb..4462e50042 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,10 @@ }, "devDependencies": { "@types/styled-components": "^5.1.26", - "cypress": "^12.12.0" + "cypress": "^12.12.0", + "msw": "^1.2.1" + }, + "msw": { + "workerDirectory": "public" } } diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000000..87e0f31b81 --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.2.1). + * @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/src/Router.tsx b/src/Router.tsx index 7e4eacaa81..aad095eacc 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,19 +1,24 @@ -import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import App from "./App"; -import ProductListPage from "./pages/ProductListPage"; +import App from './App'; +import ProductListPage from './pages/ProductListPage'; +import CartPage from './pages/CartPage'; export default function Router() { const router = createBrowserRouter( [ { - path: "/", + path: '/', element: , children: [ { - path: "/", + path: '/', element: , }, + { + path: '/cart', + element: , + }, ], }, ], diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 0000000000..306137fa1d --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,54 @@ +const getResponseBody = async (response: Response) => { + if (!response.ok) throw new Error(); + const data = await response.json(); + return data; +}; + +export const productsQuery = async () => { + const response = await fetch('/products'); + return getResponseBody(response); +}; + +export const cartQuery = async () => { + const response = await fetch('/cart-items'); + return getResponseBody(response); +}; + +export const productQuery = async (id: number) => { + const response = await fetch(`/product/${id}`); + return getResponseBody(response); +}; + +export const postCartItemQuery = async (id: number) => { + const response = await fetch('/cart-items', { + method: 'post', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ productId: id }), + }); + if (!response.ok) throw new Error(); + const header = response.headers; + const cartId = header.get('Location'); + if (!cartId) throw new Error(); + + return +cartId.replace('/cart-items/', ''); +}; + +export const patchCartItemQuantityQuery = async ( + id: number, + quantity: number +) => { + const response = await fetch(`/cart-items/${id}`, { + method: 'PATCH', + body: JSON.stringify({ quantity }), + }); + if (!response.ok) throw new Error(); +}; + +export const deleteItemQuery = async (cartId: number) => { + const response = await fetch(`/cart-items/${cartId}`, { + method: 'DELETE', + }); + if (!response.ok) throw new Error(); +}; diff --git a/src/asset/check_icon.svg b/src/asset/check_icon.svg new file mode 100644 index 0000000000..4754d61466 --- /dev/null +++ b/src/asset/check_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/asset/delete_icon.png b/src/asset/delete_icon.png new file mode 100644 index 0000000000..a34b5d827e Binary files /dev/null and b/src/asset/delete_icon.png differ diff --git a/src/asset/index.ts b/src/asset/index.ts index f15d33549e..bf5d1236dc 100644 --- a/src/asset/index.ts +++ b/src/asset/index.ts @@ -1,4 +1,5 @@ -export { ReactComponent as HeaderIc } from "./shopping-cart-white.svg"; -export { ReactComponent as AddCartIc } from "./shopping-cart-gray.svg"; -export { ReactComponent as UpButtonIc } from "./up-button.svg"; -export { ReactComponent as DownButtonIc } from "./down-button.svg"; +export { ReactComponent as HeaderIc } from './shopping-cart-white.svg'; +export { ReactComponent as AddCartIc } from './shopping-cart-gray.svg'; +export { ReactComponent as UpButtonIc } from './up-button.svg'; +export { ReactComponent as DownButtonIc } from './down-button.svg'; +export { ReactComponent as CheckIc } from './check_icon.svg'; diff --git a/src/atoms/cartSelects.ts b/src/atoms/cartSelects.ts new file mode 100644 index 0000000000..4551adece5 --- /dev/null +++ b/src/atoms/cartSelects.ts @@ -0,0 +1,42 @@ +import { atom, selector } from 'recoil'; +import { cartState } from './cartState'; + +export const cartSelectsState = atom>({ + key: 'cartSelectsState', + default: new Set(), +}); + +export const totalPrice = selector({ + key: 'totalPriceSelector', + get: ({ get }) => { + const cartSelectsSet = get(cartSelectsState); + const cart = get(cartState({ action: 'GET' })); + + return Array.from(cartSelectsSet).reduce((acc, cartSelectId) => { + const cartItem = cart.find((item) => item.id === cartSelectId); + let price = 0; + if (cartItem) { + price = cartItem.quantity * cartItem.product.price; + } + return acc + price; + }, 0); + }, +}); + +export const deliveryFee = selector({ + key: 'deliveryFeeSelector', + get: ({ get }) => { + const cartItemsPrice = get(totalPrice); + return cartItemsPrice > 30000 || cartItemsPrice === 0 ? 0 : 3000; + }, +}); + +export const checkoutPrice = selector({ + key: 'checkoutPriceSelector', + get: ({ get }) => { + const cartItemsPrice = get(totalPrice); + const deliveryPrice = get(deliveryFee); + + return cartItemsPrice + deliveryPrice; + }, +}); diff --git a/src/atoms/cartState.ts b/src/atoms/cartState.ts index f38da0c602..449dd3197e 100644 --- a/src/atoms/cartState.ts +++ b/src/atoms/cartState.ts @@ -1,16 +1,81 @@ -import { atom, selector } from 'recoil'; +import { atomFamily, selector, selectorFamily } from 'recoil'; +import { + patchCartItemQuantityQuery, + deleteItemQuery, + postCartItemQuery, + cartQuery, +} from '../api/api'; import type { CartType } from '../type/cart'; -export const cartState = atom({ - key: 'cartState', - default: [], +type GetRequestAction = { + action: 'GET'; + payload?: {}; +}; + +type PostRequestAction = { + action: 'POST'; + payload: { + productId: number; + quantity: number; + }; +}; + +type PatchRequestAction = { + action: 'PATCH'; + payload: { cartId: number; quantity: number }; +}; + +type DeleteRequestAction = { + action: 'DELETE'; + payload: { cartId: number }; +}; + +type AllRequestAction = + | GetRequestAction + | PostRequestAction + | PatchRequestAction + | DeleteRequestAction; + +export const cartRequestAction = atomFamily( + { + key: 'cartRequestAction', + default: { action: 'GET', payload: {} }, + } +); + +export const cartState = selectorFamily({ + key: 'cartQuery', + get: + (request) => + async ({ get }) => { + const { action, payload } = get(cartRequestAction(request)); // add dependency + switch (action) { + case 'POST': + const cartId = await postCartItemQuery(payload.productId); + await patchCartItemQuantityQuery(cartId, payload.quantity); + break; + case 'PATCH': + await patchCartItemQuantityQuery(payload.cartId, payload.quantity); + break; + case 'DELETE': + await deleteItemQuery(payload.cartId); + break; + case 'GET': + break; + default: + throw new Error( + 'InValid Method : 유효하지 않은 요청 메서드를 사용하고 있습니다.' + ); + } + return await cartQuery(); + }, }); -export const totalCartCountState = selector({ - key: 'totalCartCountState', - get: ({ get }) => { - const cart = get(cartState); +export const cartTotalState = selector({ + key: 'cartTotalState', + get: async ({ get }) => { + const cart = get(cartState({ action: 'GET' })); return cart.length; }, }); diff --git a/src/atoms/productState.ts b/src/atoms/productState.ts index 28e9ceab13..00638a5417 100644 --- a/src/atoms/productState.ts +++ b/src/atoms/productState.ts @@ -1,11 +1,16 @@ -import { selector } from "recoil"; +import { selector, selectorFamily } from 'recoil'; +import { productQuery, productsQuery } from '../api/api'; export const products = selector({ - key: "products", + key: 'productsSelector', get: async () => { - const response = await fetch("./products.json"); - if (!response.ok) throw new Error(); - const data = await response.json(); - return data; + return productsQuery(); + }, +}); + +export const product = selectorFamily({ + key: 'productSelector', + get: (id: number) => async () => { + return productQuery(id); }, }); diff --git a/src/components/CartPage/CartList.tsx b/src/components/CartPage/CartList.tsx new file mode 100644 index 0000000000..831c3a370f --- /dev/null +++ b/src/components/CartPage/CartList.tsx @@ -0,0 +1,55 @@ +import styled from 'styled-components'; +import CartListItem from './CartListItem'; +import SelectCartItem from './SelectCartItem'; +import { useRecoilValue_TRANSITION_SUPPORT_UNSTABLE } from 'recoil'; +import { cartState } from '../../atoms/cartState'; +import { Suspense, useTransition } from 'react'; +import CartListHeader from './CartList/CartListHeader'; +import ErrorBoundary from '../common/ErrorBoundary'; +import { CartType } from '../../type/cart'; + +interface CartListItemsProps { + cart: CartType[]; +} +function CartListItems({ cart }: CartListItemsProps) { + return ( + + {cart.map((cartItem) => ( + + ))} + + ); +} + +export default function CartList() { + const cart = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE( + cartState({ action: 'GET' }) + ); + const [inTrans] = useTransition(); + + return ( + + + 든든 배송 상품 (0개)}> + + + {inTrans ?
loading...
: null} + + + +
+ + + +
+ ); +} + +const CartListContainer = styled.div` + width: 55%; +`; + +const Header = styled.h3` + ${({ theme }) => theme.fonts.cartHeading}; + border-bottom: 4px solid ${({ theme }) => theme.colors.gray200}; +`; diff --git a/src/components/CartPage/CartList/CartListHeader.tsx b/src/components/CartPage/CartList/CartListHeader.tsx new file mode 100644 index 0000000000..fd44362e88 --- /dev/null +++ b/src/components/CartPage/CartList/CartListHeader.tsx @@ -0,0 +1,20 @@ +import styled from 'styled-components'; +import { cartTotalState } from '../../../atoms/cartState'; +import { useRecoilValue } from 'recoil'; +import { useTransition } from 'react'; + +export default function CartListHeader() { + const cartTotal = useRecoilValue(cartTotalState); + const [isPending] = useTransition(); + + return ( + + 든든 배송 상품 ({isPending ? 0 : cartTotal}개) + + ); +} + +const CartListHeaderContainer = styled.h3` + ${({ theme }) => theme.fonts.cartHeading}; + border-bottom: 4px solid ${({ theme }) => theme.colors.gray200}; +`; diff --git a/src/components/CartPage/CartListItem.tsx b/src/components/CartPage/CartListItem.tsx new file mode 100644 index 0000000000..31e4240ebf --- /dev/null +++ b/src/components/CartPage/CartListItem.tsx @@ -0,0 +1,123 @@ +import styled from 'styled-components'; + +import QuantityCounter from '../common/QuantityCounter'; +import CheckIconImage from '../../asset/check_icon.svg'; +import useCount from '../../hooks/useCount'; +import { Product } from '../../type/product'; +import DeleteButton from './DeleteButton'; +import { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { cartSelectsState } from '../../atoms/cartSelects'; +import { cartRequestAction } from '../../atoms/cartState'; +import useDebounce from '../../hooks/useDebounce'; + +interface CartListItemProps { + id: number; + quantity: number; + product: Product; +} + +export default function CartListItem({ + id, + quantity, + product, +}: CartListItemProps) { + const { count, setCount } = useCount(quantity); + const { name, imageUrl, price } = product; + const [cartSelects, setCartSelects] = useRecoilState(cartSelectsState); + const setRequestAction = useSetRecoilState( + cartRequestAction({ action: 'GET' }) + ); + const isChecked = cartSelects.has(id); + const countDebounce = useDebounce(() => { + setRequestAction({ + action: 'PATCH', + payload: { cartId: id, quantity: count.value }, + }); + }); + + const onSelectChange = () => { + const newCartSelects = Array.from(cartSelects); + const newCartSelectSet = new Set(newCartSelects); + + if (!isChecked) { + newCartSelectSet.add(id); + } else { + newCartSelectSet.delete(id); + } + setCartSelects(newCartSelectSet); + }; + + useEffect(() => { + countDebounce(); + }, [count]); + + return ( + + + + + {name} + + + + + + + {price.toLocaleString()}원 + + + ); +} + +const CartListItemContainer = styled.li` + display: flex; + justify-content: space-between; + width: 100%; + padding: 2rem 0; +`; + +const CartInfoContainer = styled.div` + display: flex; +`; + +const ProductImg = styled.img` + width: 14.4rem; + height: 14.7rem; +`; + +const ProductName = styled.p` + ${({ theme }) => theme.fonts.cartProductName} +`; + +const ProductPrice = styled.p` + ${({ theme }) => theme.fonts.cartProductPrice} +`; + +const SelectBox = styled.input` + appearance: none; + width: 2.8rem; + height: 2.8rem; + border: 1px solid ${({ theme }) => theme.colors.blue_green}; + border-radius: 2px; + cursor: pointer; + + &:checked { + border: 1px solid ${({ theme }) => theme.colors.primary}; + background-color: ${({ theme }) => theme.colors.primary}; + background-image: url(${CheckIconImage}); + background-repeat: no-repeat; + background-position: center; + } +`; + +const CartOptionContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: space-between; +`; diff --git a/src/components/CartPage/DeleteButton.tsx b/src/components/CartPage/DeleteButton.tsx new file mode 100644 index 0000000000..fdb1c844f3 --- /dev/null +++ b/src/components/CartPage/DeleteButton.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import DeleteButtonImage from '../../asset/delete_icon.png'; +import { useRecoilState, useSetRecoilState } from 'recoil'; +import { cartRequestAction } from '../../atoms/cartState'; +import { cartSelectsState } from '../../atoms/cartSelects'; + +interface DeleteButtonProps { + cartId: number; +} + +export default function DeleteButton({ cartId }: DeleteButtonProps) { + const setCartRequestActions = useSetRecoilState( + cartRequestAction({ action: 'GET' }) + ); + const [cartSelects, setCartSelects] = useRecoilState(cartSelectsState); + + const onDeleteCartItem = () => { + setCartRequestActions({ + action: 'DELETE', + payload: { cartId: cartId }, + }); + const newCartSelects = new Set(Array.from(cartSelects)); + newCartSelects.delete(cartId); + setCartSelects(newCartSelects); + }; + + return ( + + + + ); +} + +const DeleteButtonContainer = styled.button` + width: 2.4rem; + height: 2.4rem; + background-color: transparent; + cursor: pointer; +`; + +const DeleteButtonIcon = styled.img` + width: 2.4rem; + height: 2.4rem; +`; diff --git a/src/components/CartPage/SelectCartItem.tsx b/src/components/CartPage/SelectCartItem.tsx new file mode 100644 index 0000000000..6c4aedbb3c --- /dev/null +++ b/src/components/CartPage/SelectCartItem.tsx @@ -0,0 +1,108 @@ +import styled from 'styled-components'; +import CheckIconImage from '../../asset/check_icon.svg'; +import { + cartRequestAction, + cartState, + cartTotalState, +} from '../../atoms/cartState'; +import { + useRecoilState, + useRecoilValue_TRANSITION_SUPPORT_UNSTABLE, + useSetRecoilState, +} from 'recoil'; +import { cartSelectsState } from '../../atoms/cartSelects'; +import { Suspense, useTransition } from 'react'; +import { CartType } from '../../type/cart'; +import ErrorBoundary from '../common/ErrorBoundary'; + +export default function SelectCartItem() { + const cart = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE( + cartState({ action: 'GET' }) + ); + const cartTotal = useRecoilValue_TRANSITION_SUPPORT_UNSTABLE(cartTotalState); + + const [cartSelects, setCartSelects] = useRecoilState(cartSelectsState); + const setRequestAction = useSetRecoilState( + cartRequestAction({ action: 'GET' }) + ); + const isCheckAll = cartSelects.size === cartTotal && cartTotal !== 0; + const [inTrans, startTrans] = useTransition(); + + const deleteSelectCartItem = () => { + startTrans(() => { + cartSelects.forEach((cartSelect) => { + setRequestAction({ + action: 'DELETE', + payload: { cartId: cartSelect }, + }); + setCartSelects(new Set()); + }); + }); + }; + + const onSelectAllCart = () => { + if (!isCheckAll) { + const newCartSelects = cart.map((cartItem) => cartItem.id); + setCartSelects(new Set(newCartSelects)); + } + if (isCheckAll && cartSelects.size === cartTotal) { + setCartSelects(new Set()); + } + }; + + return ( + + {inTrans ?
loading..
: null} + + + + + 전체선택({cartSelects.size}/{cartTotal}) + + + 선택 삭제 + + + +
+ ); +} + +const SelectCartItemContainer = styled.div` + display: flex; + align-items: center; +`; + +const SelectBox = styled.input` + appearance: none; + width: 2.8rem; + height: 2.8rem; + border: 1px solid ${({ theme }) => theme.colors.blue_green}; + border-radius: 2px; + cursor: pointer; + + &:checked { + border: 1px solid ${({ theme }) => theme.colors.primary}; + background-color: ${({ theme }) => theme.colors.primary}; + background-image: url(${CheckIconImage}); + background-repeat: no-repeat; + background-position: center; + } +`; + +const Text = styled.span` + ${({ theme }) => theme.fonts.name} + margin: 0 1rem; +`; + +const SelectDeleteButton = styled.button` + background-color: transparent; + ${({ theme }) => theme.fonts.name} + border: 1px solid ${({ theme }) => theme.colors.gray200}; + padding: 0.5rem 1rem; + cursor: pointer; +`; diff --git a/src/components/CartPage/SubTotal.tsx b/src/components/CartPage/SubTotal.tsx new file mode 100644 index 0000000000..2409911418 --- /dev/null +++ b/src/components/CartPage/SubTotal.tsx @@ -0,0 +1,42 @@ +import styled from 'styled-components'; + +import { Suspense } from 'react'; +import SubTotalContent from './SubTotal/SubTotalContent'; + +export default function SubTotal() { + return ( + + 결제예상금액 + loading...}> + + + 주문하기 + + ); +} + +const SubTotalContainer = styled.div` + display: flex; + flex-direction: column; + border: 1px solid #ddd; + width: 35%; + align-items: center; +`; + +const Title = styled.h3` + width: 100%; + text-align: center; + ${({ theme }) => theme.fonts.cartHeading}; + padding: 1rem 2rem; + border-bottom: 3px solid ${({ theme }) => theme.colors.gray100}; +`; + +const CheckoutButton = styled.button` + color: ${({ theme }) => theme.colors.white}; + background-color: ${({ theme }) => theme.colors.primary}; + width: 80%; + padding: 2rem 0; + margin: 4rem 0; + ${({ theme }) => theme.fonts.cartHeading}; + cursor: pointer; +`; diff --git a/src/components/CartPage/SubTotal/CheckoutPrice.tsx b/src/components/CartPage/SubTotal/CheckoutPrice.tsx new file mode 100644 index 0000000000..5b185dd87f --- /dev/null +++ b/src/components/CartPage/SubTotal/CheckoutPrice.tsx @@ -0,0 +1,16 @@ +import { useRecoilValue } from 'recoil'; +import { checkoutPrice } from '../../../atoms/cartSelects'; +import styled from 'styled-components'; + +export default function CheckoutPrice() { + const checkout = useRecoilValue(checkoutPrice); + return ( + + {checkout.toLocaleString()}원 + + ); +} + +const CheckoutPriceContainer = styled.span` + ${({ theme }) => theme.fonts.subtotalContent}; +`; diff --git a/src/components/CartPage/SubTotal/DeliveryPrice.tsx b/src/components/CartPage/SubTotal/DeliveryPrice.tsx new file mode 100644 index 0000000000..baf2426cdf --- /dev/null +++ b/src/components/CartPage/SubTotal/DeliveryPrice.tsx @@ -0,0 +1,17 @@ +import { useRecoilValue } from 'recoil'; +import styled from 'styled-components'; +import { deliveryFee } from '../../../atoms/cartSelects'; + +export default function DeliveryPrice() { + const deliveryPrice = useRecoilValue(deliveryFee); + + return ( + + {deliveryPrice.toLocaleString()}원 + + ); +} + +const DeliveryPriceContainer = styled.span` + ${({ theme }) => theme.fonts.subtotalContent}; +`; diff --git a/src/components/CartPage/SubTotal/SubTotalContent.tsx b/src/components/CartPage/SubTotal/SubTotalContent.tsx new file mode 100644 index 0000000000..e9d6e7ab9f --- /dev/null +++ b/src/components/CartPage/SubTotal/SubTotalContent.tsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; +import TotalPrice from './TotalPrice'; +import DeliveryPrice from './DeliveryPrice'; +import CheckoutPrice from './CheckoutPrice'; +import { Suspense } from 'react'; + +function Loading() { + return 0원; +} +export default function SubTotalContent() { + return ( + <> + + 총 상품가격 + }> + + + + + 총 배송비 + }> + + + + + 총 주문금액 + }> + + + + + ); +} + +const Text = styled.span` + ${({ theme }) => theme.fonts.subtotalContent}; +`; + +const Paragraph = styled.p` + display: flex; + justify-content: space-between; + width: 80%; + margin: 1.3rem 0; +`; diff --git a/src/components/CartPage/SubTotal/TotalPrice.tsx b/src/components/CartPage/SubTotal/TotalPrice.tsx new file mode 100644 index 0000000000..7a88fb7285 --- /dev/null +++ b/src/components/CartPage/SubTotal/TotalPrice.tsx @@ -0,0 +1,17 @@ +import { useRecoilValue } from 'recoil'; +import styled from 'styled-components'; +import { totalPrice } from '../../../atoms/cartSelects'; + +export default function TotalPrice() { + const cartItemsPrice = useRecoilValue(totalPrice); + + return ( + + {cartItemsPrice.toLocaleString()}원 + + ); +} + +const TotalPriceContainer = styled.span` + ${({ theme }) => theme.fonts.subtotalContent}; +`; diff --git a/src/components/CartQuantity.tsx b/src/components/CartQuantity.tsx index 08a66d639a..7f6ff44963 100644 --- a/src/components/CartQuantity.tsx +++ b/src/components/CartQuantity.tsx @@ -1,19 +1,22 @@ -import { useRecoilValue } from 'recoil'; import styled from 'styled-components'; -import { totalCartCountState } from '../atoms/cartState'; +import { useNavigate } from 'react-router-dom'; +import QuantityCircle from './ProductListPage/QuantityCircle'; +import { Suspense } from 'react'; interface CartQuantityProps { user?: string; } export default function CartQuantity({ user }: CartQuantityProps) { - const totalCart = useRecoilValue(totalCartCountState); + const navigate = useNavigate(); return ( - + navigate('/cart')}>
{user && `${user}의 `}장바구니
- {totalCart} + ..}> + +
); } @@ -22,6 +25,7 @@ const CartQuantityContainer = styled.div` display: flex; justify-content: center; align-items: center; + cursor: pointer; ${({ theme }) => theme.fonts.h2} color : ${({ theme }) => theme.colors.white} diff --git a/src/components/ProductListPage/ProductItem.tsx b/src/components/ProductListPage/ProductItem.tsx index 45ce320f12..85c7d85272 100644 --- a/src/components/ProductListPage/ProductItem.tsx +++ b/src/components/ProductListPage/ProductItem.tsx @@ -17,7 +17,7 @@ export default function ProductItem({ name, price, }: ProductItemProps) { - const { isSelected, selectProductItem, addCartProductItem, quantityRef } = + const { isSelected, selectProductItem, addCartProductItem, count, setCount } = useAddCart(); return ( @@ -29,7 +29,7 @@ export default function ProductItem({ {price.toLocaleString()}원 {isSelected ? ( - + ) : ( diff --git a/src/components/ProductListPage/ProductList.tsx b/src/components/ProductListPage/ProductList.tsx index 63c1dd0add..84980f8aca 100644 --- a/src/components/ProductListPage/ProductList.tsx +++ b/src/components/ProductListPage/ProductList.tsx @@ -11,7 +11,7 @@ export default function ProductList() { return ( {products.map((product) => ( - + ))} ); @@ -21,7 +21,6 @@ const ProductListContainer = styled.ul` display: grid; padding: 6rem; grid-template-columns: repeat(4, 28.2rem); - gap: 4rem; @media only screen and (max-width: 1200px) { diff --git a/src/components/ProductListPage/QuantityCircle.tsx b/src/components/ProductListPage/QuantityCircle.tsx new file mode 100644 index 0000000000..90126ae449 --- /dev/null +++ b/src/components/ProductListPage/QuantityCircle.tsx @@ -0,0 +1,24 @@ +import { useRecoilValue } from 'recoil'; +import { cartTotalState } from '../../atoms/cartState'; + +import styled from 'styled-components'; + +export default function QuantityCircle() { + const totalCart = useRecoilValue(cartTotalState); + + return {totalCart}; +} + +const Quantity = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 2.6rem; + height: 2.6rem; + padding-top: 0.3rem; + margin-left: 2rem; + + border-radius: 50%; + background-color: ${({ theme }) => theme.colors.blue_green}; +`; diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx index 68fa982748..8976c64d89 100644 --- a/src/components/common/ErrorBoundary.tsx +++ b/src/components/common/ErrorBoundary.tsx @@ -37,7 +37,7 @@ const ErrorWrapper = styled.div` flex-direction: column; align-items: center; justify-content: center; - + ${({ theme }) => theme.fonts.warringMessage} width: 100%; height: 100vh; `; diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index a895d18b8f..2631baa083 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -1,12 +1,15 @@ -import { PropsWithChildren } from "react"; -import styled from "styled-components"; +import { PropsWithChildren } from 'react'; +import { useNavigate } from 'react-router-dom'; +import styled from 'styled-components'; -import { HeaderIc } from "../../asset"; +import { HeaderIc } from '../../asset'; export default function Header({ children }: PropsWithChildren) { + const navigate = useNavigate(); + return ( - + navigate('/')}> WOOSINSA @@ -32,4 +35,5 @@ const Title = styled.h1` const TitleWrapper = styled.div` display: flex; + cursor: pointer; `; diff --git a/src/components/common/QuantityCounter.tsx b/src/components/common/QuantityCounter.tsx index cef14ae459..d56ff8faee 100644 --- a/src/components/common/QuantityCounter.tsx +++ b/src/components/common/QuantityCounter.tsx @@ -1,65 +1,58 @@ -import { forwardRef } from 'react'; import styled from 'styled-components'; -import type { CounterAction } from '../../type/counter'; import { DownButtonIc, UpButtonIc } from '../../asset'; -import { ACTION_DECREASE, ACTION_INCREASE } from '../../constants/counter'; -import { ERROR } from '../../constants/error'; -import { isForwardedRef, isRefCurrent } from '../../utils/refTypeGuard'; -import { fillBlankInput, validateNumberRange } from '../../utils/validation'; - -const unknownCountAction = (action: never): never => { - throw new Error(ERROR.INVALID_ACTION); -}; - -const changeCount = (current: HTMLInputElement, action: CounterAction) => { - const prevValue = +current.value; - switch (action) { - case ACTION_INCREASE: - current.value = (prevValue + 1).toString(); - break; - case ACTION_DECREASE: - if (prevValue < 2) return; - current.value = (prevValue - 1).toString(); - break; - default: - unknownCountAction(action); - } -}; - -const QuantityCounter = forwardRef(function (_, quantityRef) { - const changeQuantityByAction = (action: CounterAction) => { - if (!isForwardedRef(quantityRef)) return; - if (!isRefCurrent(quantityRef.current)) return; - - changeCount(quantityRef.current, action); - }; - +import { + ACTION_CHANGE, + ACTION_DECREASE, + ACTION_INCREASE, +} from '../../constants/counter'; +import { CountAction, CountState } from '../../type/counter'; +import React from 'react'; + +interface QuantityCounterProps { + count: CountState; + setCount: React.Dispatch; +} + +export default function QuantityCounter({ + count, + setCount, +}: QuantityCounterProps) { return ( - - - changeQuantityByAction(ACTION_INCREASE)}> - - - changeQuantityByAction(ACTION_DECREASE)}> - - - + + + setCount({ action: ACTION_CHANGE, payload: e.target.value }) + } + /> + + setCount({ action: ACTION_INCREASE, payload: '' })} + > + + + setCount({ action: ACTION_DECREASE, payload: '' })} + > + + + + + {count.status === 'INVALID' && ( + ⚠️ 장바구니에 담을 수량을 1개 이상 적어주세요. + )} ); -}); - -export default QuantityCounter; +} const QuantityCounterContainer = styled.div` - display: flex; + position: relative; +`; +const QuantityCounterWrapper = styled.div` + display: flex; width: 6.4rem; height: 4.3rem; `; @@ -88,3 +81,14 @@ const CountButton = styled.button` background-color: transparent; border: 0.1rem solid ${({ theme }) => theme.colors.gray100}; `; + +const ErrorBox = styled.div` + position: absolute; + top: 5rem; + width: 15rem; + padding: 1rem; + background-color: white; + ${({ theme }) => theme.fonts.warringMessage} + border: 1px solid ${({ theme }) => theme.colors.gray200}; + box-shadow: 2px 2px ${({ theme }) => theme.colors.gray200}; +`; diff --git a/src/constants/counter.ts b/src/constants/counter.ts index 36abe96af2..557bf9d873 100644 --- a/src/constants/counter.ts +++ b/src/constants/counter.ts @@ -1,2 +1,3 @@ -export const ACTION_INCREASE = "INCREASE"; -export const ACTION_DECREASE = "DECREASE"; +export const ACTION_INCREASE = 'INCREASE'; +export const ACTION_DECREASE = 'DECREASE'; +export const ACTION_CHANGE = 'CHANGE'; diff --git a/src/hooks/useAddCart.ts b/src/hooks/useAddCart.ts index 7abbc78f4e..b87b3cebd1 100644 --- a/src/hooks/useAddCart.ts +++ b/src/hooks/useAddCart.ts @@ -1,44 +1,28 @@ -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { useSetRecoilState } from 'recoil'; -import { cartState } from '../atoms/cartState'; -import { CartType } from '../type/cart'; +import { cartRequestAction } from '../atoms/cartState'; +import useCount from './useCount'; export function useAddCart() { const [isSelected, setIsSelected] = useState(false); - const quantityRef = useRef(null); - const setCart = useSetRecoilState(cartState); + const setCartRequestActions = useSetRecoilState( + cartRequestAction({ action: 'GET' }) + ); + const { count, setCount } = useCount(); const selectProductItem = () => { setIsSelected(true); }; - const createChangedCart = (prevCart: CartType[], cartItem: CartType) => { - const index = prevCart.findIndex( - (prevItem) => prevItem.productId === cartItem.productId - ); - if (index === -1) return [...prevCart, cartItem]; - - return [ - ...prevCart.slice(0, index), - cartItem, - ...prevCart.slice(index + 1), - ]; - }; - - const addCartProductItem = (id: number) => { + const addCartProductItem = async (productId: number) => { setIsSelected(false); - const cartItem: CartType = { - productId: id, - quantity: 0, - }; - - if (quantityRef.current) { - cartItem.quantity = +quantityRef.current.value; - } - setCart((prev) => createChangedCart(prev, cartItem)); + setCartRequestActions({ + action: 'POST', + payload: { productId: productId, quantity: count.value }, + }); }; - return { isSelected, selectProductItem, addCartProductItem, quantityRef }; + return { isSelected, selectProductItem, addCartProductItem, count, setCount }; } diff --git a/src/hooks/useCount.ts b/src/hooks/useCount.ts new file mode 100644 index 0000000000..349be2fc0e --- /dev/null +++ b/src/hooks/useCount.ts @@ -0,0 +1,38 @@ +import { useReducer } from 'react'; + +import { CountAction, CountState } from '../type/counter'; +import { ERROR } from '../constants/error'; +import { + ACTION_CHANGE, + ACTION_DECREASE, + ACTION_INCREASE, +} from '../constants/counter'; + +const unknownCountAction = (action: never): never => { + throw new Error(ERROR.INVALID_ACTION); +}; + +const countDispatcher = (state: CountState, next: CountAction): CountState => { + const { action, payload } = next; + switch (action) { + case ACTION_INCREASE: + return { value: state.value + 1, status: 'VALID' }; + case ACTION_DECREASE: + return { value: Math.max(state.value - 1, 1), status: 'VALID' }; + case ACTION_CHANGE: + const newValue = payload.replaceAll(/\D+/g, ''); + const newStatus = newValue ? 'VALID' : 'INVALID'; + return { value: +newValue, status: newStatus }; + default: + return unknownCountAction(action); + } +}; + +export default function useCount(initValue = 1) { + const [count, setCount] = useReducer(countDispatcher, { + value: initValue, + status: 'VALID', + }); + + return { count, setCount }; +} diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000000..a7a8744243 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,15 @@ +import { useRef } from 'react'; + +const useDebounce = (callback: VoidFunction) => { + const debounceId = useRef(); + return () => { + if (debounceId.current) { + clearTimeout(debounceId.current); + } + debounceId.current = setTimeout(() => { + callback(); + }, 200); + }; +}; + +export default useDebounce; diff --git a/src/index.tsx b/src/index.tsx index 830cdbc0ea..4c7aeea7b4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,17 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; -import Router from "./Router"; +import Router from './Router'; + +if (process.env.NODE_ENV === 'development') { + const { worker } = require('./mocks/browser'); + worker.start(); +} const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement + document.getElementById('root') as HTMLElement ); + root.render( diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 0000000000..750e031c2b --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,4 @@ +import { setupWorker } from 'msw'; +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000000..eed4b1c3a0 --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,17 @@ +import { + deleteCartItem, + getCartItems, + patchCartItem, + postCartItem, +} from './rest/cart'; +import { getProducts, getProduct, putProduct } from './rest/product'; + +export const handlers = [ + getProducts, + getProduct, + putProduct, + getCartItems, + postCartItem, + deleteCartItem, + patchCartItem, +]; diff --git a/src/mocks/products.ts b/src/mocks/products.ts new file mode 100644 index 0000000000..03d585b967 --- /dev/null +++ b/src/mocks/products.ts @@ -0,0 +1,86 @@ +export let products = [ + { + id: 1, + name: '[23 F/W] Tom Brown', + price: 930000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20221017/2867105/2867105_1_220.jpg', + }, + { + id: 2, + name: '[23 F/W] Off White', + price: 630000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20220726/2682017/2682017_1_220.jpg', + }, + { + id: 3, + name: '[22 F/W] Pants', + price: 450000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20230509/3284078/3284078_16836104882197_320.jpg', + }, + { + id: 4, + name: '[Gucci] Leather Jacket', + price: 1500000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20200901/1576682/1576682_6_320.jpg', + }, + { + id: 5, + name: 'Messenger Bag', + price: 200000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20220426/2514819/2514819_3_320.jpg', + }, + { + id: 6, + name: 'chelsea boots', + price: 4500000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20200828/1568047/1568047_24_320.jpg', + }, + { + id: 7, + name: '마르지엘라 [F/W 23]', + price: 10000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20210204/1778523/1778523_2_125.jpg', + }, + { + id: 8, + name: 'RC B019 BLACK GLASS', + price: 20000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20230126/3039520/3039520_16762638204628_220.jpg', + }, + { + id: 9, + name: 'ACTIVITY CLUB PIGMENT OVER FIT TEE', + price: 40000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20221017/2867105/2867105_1_220.jpg', + }, + { + id: 10, + name: '크로스백 - 블랙 / BG252LL095BK999', + price: 1470000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20230327/3181575/3181575_16830053095177_500.jpg', + }, + { + id: 11, + name: '남성 와펜 패치 소프트 쉘 집업 재킷', + price: 611000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20220404/2463979/2463979_1_500.jpg', + }, + { + id: 12, + name: '척 70 빈티지 캔버스 러쉬블루 168514C ', + price: 53000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20200622/1493265/1493265_1_500.jpg', + }, +]; diff --git a/src/mocks/rest/cart.ts b/src/mocks/rest/cart.ts new file mode 100644 index 0000000000..6be671b198 --- /dev/null +++ b/src/mocks/rest/cart.ts @@ -0,0 +1,75 @@ +import { rest } from 'msw'; +import { products } from '../products'; + +let cart = [ + { + id: 1, + quantity: 5, + product: { + id: 1, + name: '[23 F/W] Tom Brown', + price: 930000, + imageUrl: + 'https://image.msscdn.net/images/goods_img/20221017/2867105/2867105_1_220.jpg', + }, + }, +]; + +let cartId = 1; + +export const getCartItems = rest.get('/cart-items', (_, res, ctx) => { + return res(ctx.status(200), ctx.json(cart)); +}); + +export const postCartItem = rest.post('/cart-items', async (req, res, ctx) => { + const { productId } = await req.json(); + const cartItem = cart.find((cartItem) => cartItem.product.id === +productId); + const targetProduct = products.find((product) => product.id === +productId); + + if (!targetProduct) { + return res(ctx.status(400)); + } + + if (!cartItem) { + const newCartItem = { + id: ++cartId, + quantity: 1, + product: targetProduct, + }; + cart = [...cart, newCartItem]; + return res( + ctx.status(201), + ctx.set('Location', `/cart-items/${newCartItem.id}`) + ); + } + + cartItem.quantity = 1; + return res( + ctx.status(200), + ctx.set('Location', `/cart-items/${cartItem.id}`) + ); +}); + +export const deleteCartItem = rest.delete( + '/cart-items/:cartItemId', + (req, res, ctx) => { + cart = cart.filter((cartItem) => cartItem.id !== +req.params.cartItemId); + + return res(ctx.status(204)); + } +); + +export const patchCartItem = rest.patch( + '/cart-items/:cartId', + async (req, res, ctx) => { + const data = await req.json(); + const cartItem = cart.find( + (cartItem) => cartItem.id === +req.params.cartId + ); + + if (!cartItem) return res(ctx.status(400)); + + cartItem.quantity = data.quantity; + return res(ctx.status(200)); + } +); diff --git a/src/mocks/rest/product.ts b/src/mocks/rest/product.ts new file mode 100644 index 0000000000..acb257cb98 --- /dev/null +++ b/src/mocks/rest/product.ts @@ -0,0 +1,34 @@ +import { rest } from 'msw'; +import { products } from '../products'; + +const handleNonExistData = (assertData: any, responseData: any) => { + if (!assertData) { + return { statusCode: 400, responseData: {} }; + } + return { statusCode: 200, responseData }; +}; + +export const getProduct = rest.get('/products/:productId', (req, res, ctx) => { + const productResponseData = products.find( + (product) => product.id === +req.params.productId + ); + + const { statusCode, responseData } = handleNonExistData( + productResponseData, + productResponseData + ); + + return res(ctx.status(statusCode), ctx.json(responseData)); +}); + +export const putProduct = rest.put('/products/:productId', (req, res, ctx) => { + let matchProduct = products.find( + (product) => product.id === +req.params.productId + ); + const { statusCode, responseData } = handleNonExistData(matchProduct, {}); + return res(ctx.status(statusCode), ctx.json(responseData)); +}); + +export const getProducts = rest.get('/products', (_, res, ctx) => { + return res(ctx.status(200), ctx.json(products)); +}); diff --git a/src/pages/CartPage.tsx b/src/pages/CartPage.tsx new file mode 100644 index 0000000000..f85915f630 --- /dev/null +++ b/src/pages/CartPage.tsx @@ -0,0 +1,38 @@ +import styled from 'styled-components'; +import CartList from '../components/CartPage/CartList'; +import SubTotal from '../components/CartPage/SubTotal'; + +export default function CartPage() { + return ( + + 장바구니 + + + + + + ); +} + +const CartPageContainer = styled.div` + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + color: ${({ theme }) => theme.colors.primary}; +`; + +const CartPageHeader = styled.div` + margin: 0 11rem; + padding: 3rem; + text-align: center; + ${({ theme }) => theme.fonts.pageTitle} + border-bottom: 4px solid ${({ theme }) => theme.colors.primary}; +`; + +const CartPageBody = styled.div` + display: flex; + padding-top: 4rem; + margin: 0 11rem; + justify-content: space-between; +`; diff --git a/src/style/theme.ts b/src/style/theme.ts index 59dc81a6b9..ba615763ef 100644 --- a/src/style/theme.ts +++ b/src/style/theme.ts @@ -1,13 +1,13 @@ -import { DefaultTheme } from "styled-components"; +import { DefaultTheme } from 'styled-components'; -import type { FontType, StyleType } from "../type/theme"; +import type { FontType, StyleType } from '../type/theme'; const colors: StyleType = { - primary: "#333333", - gray100: "#dddddd", - gray200: "#aaaaaa", - blue_green: "#04c09e", - white: "#ffffff", + primary: '#333333', + gray100: '#dddddd', + gray200: '#aaaaaa', + blue_green: '#04c09e', + white: '#ffffff', }; function FONT({ family, weight, size, lineHeight }: FontType): string { @@ -21,29 +21,65 @@ function FONT({ family, weight, size, lineHeight }: FontType): string { const fonts: StyleType = { h1: FONT({ - family: "Noto Sans KR, sans-serif", + family: 'Noto Sans KR, sans-serif', weight: 600, size: 4, lineHeight: 5.8, }), h2: FONT({ - family: "Noto Sans KR, sans-serif", + family: 'Noto Sans KR, sans-serif', weight: 500, size: 2.4, lineHeight: 1.2, }), name: FONT({ - family: "Noto Sans KR, sans-serif", + family: 'Noto Sans KR, sans-serif', weight: 400, size: 1.6, lineHeight: 2.2, }), price: FONT({ - family: "Noto Sans KR, sans-serif", + family: 'Noto Sans KR, sans-serif', weight: 400, size: 2, lineHeight: 2.7, }), + warringMessage: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 400, + size: 1.2, + lineHeight: 1.6, + }), + pageTitle: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 700, + size: 3.2, + lineHeight: 3.7, + }), + cartHeading: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 400, + size: 2.4, + lineHeight: 3.3, + }), + cartProductName: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 400, + size: 2.0, + lineHeight: 2.4, + }), + cartProductPrice: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 400, + size: 1.6, + lineHeight: 2.4, + }), + subtotalContent: FONT({ + family: 'Noto Sans KR, sans-serif', + weight: 700, + size: 2.0, + lineHeight: 2.7, + }), }; export const theme: DefaultTheme = { diff --git a/src/test/useCount.test.ts b/src/test/useCount.test.ts new file mode 100644 index 0000000000..15076d84ec --- /dev/null +++ b/src/test/useCount.test.ts @@ -0,0 +1,61 @@ +import { renderHook } from '@testing-library/react'; +import useCount from '../hooks/useCount'; +import { + ACTION_CHANGE, + ACTION_DECREASE, + ACTION_INCREASE, +} from '../constants/counter'; +import { act } from 'react-dom/test-utils'; + +describe('useCount 테스트', () => { + it('count의 초기 상태는 1을 가진다.', () => { + const { result } = renderHook(() => useCount()); + expect(result.current.count.value).toBe(1); + }); + + it('INCREASE 액션으로 1의 값만큼 올린다.', () => { + const { result } = renderHook(() => useCount()); + expect(result.current.count.value).toBe(1); + act(() => { + result.current.setCount({ action: ACTION_INCREASE, payload: '' }); + }); + expect(result.current.count.value).toBe(2); + }); + + it('DECREASE 액션으로 숫자를 1의 값만큼 내린다.', () => { + const { result } = renderHook(() => useCount()); + expect(result.current.count.value).toBe(1); + act(() => { + result.current.setCount({ action: ACTION_INCREASE, payload: '' }); + result.current.setCount({ action: ACTION_DECREASE, payload: '' }); + }); + expect(result.current.count.value).toBe(1); + }); + + it('CHANGE 액션으로 원하는 숫자값으로 값을 변경 할 수 있다.', () => { + const { result } = renderHook(() => useCount()); + act(() => { + result.current.setCount({ action: ACTION_CHANGE, payload: '120' }); + }); + expect(result.current.count.value).toBe(120); + }); + + it('CHANGE 액션으로 잘못된 값을 전달할 경우, 숫자값을 제외한 값은 공백으로 바꾼다.', () => { + const { result } = renderHook(() => useCount()); + act(() => { + result.current.setCount({ action: ACTION_CHANGE, payload: 'dff1bb12' }); + }); + expect(result.current.count.value).toBe(112); + }); + + it('CHANGE 액션으로 잘못된 값을 전달할 경우, 모두 숫자가 아닌경우 0으로 설정한다.', () => { + const { result } = renderHook(() => useCount()); + act(() => { + result.current.setCount({ + action: ACTION_CHANGE, + payload: 'ㄴ어ㅣㅇㄴ러', + }); + }); + expect(result.current.count.value).toBe(0); + }); +}); diff --git a/src/type/cart.ts b/src/type/cart.ts index 9b21f801bb..bdf29d0062 100644 --- a/src/type/cart.ts +++ b/src/type/cart.ts @@ -1,4 +1,7 @@ +import { Product } from './product'; + export interface CartType { - productId: number; + id: number; quantity: number; + product: Product; } diff --git a/src/type/counter.ts b/src/type/counter.ts index cfba866164..3e84dabfa0 100644 --- a/src/type/counter.ts +++ b/src/type/counter.ts @@ -1 +1,11 @@ -export type CounterAction = "INCREASE" | "DECREASE"; +export type CounterDispatchAction = 'INCREASE' | 'DECREASE' | 'CHANGE'; + +export interface CountAction { + action: CounterDispatchAction; + payload: string; +} + +export interface CountState { + value: number; + status: 'VALID' | 'INVALID'; +} diff --git a/src/utils/refTypeGuard.ts b/src/utils/refTypeGuard.ts deleted file mode 100644 index 6aba2907e0..0000000000 --- a/src/utils/refTypeGuard.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MutableRefObject } from "react"; - -export function isForwardedRef( - ref: React.ForwardedRef -): ref is MutableRefObject { - return typeof ref !== "function" && ref !== null; -} - -export function isRefCurrent(current: T): current is T { - return current !== null; -} diff --git a/src/utils/validation.ts b/src/utils/validation.ts deleted file mode 100644 index 0a1ef106a3..0000000000 --- a/src/utils/validation.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const validateNumberRange = (e: React.ChangeEvent) => { - const rule = /[^0-9]+/g; - e.target.value = e.target.value.replaceAll(rule, ""); - - if (e.target.value === "0") { - e.target.value = "1"; - } -}; - -export const fillBlankInput = (e: React.ChangeEvent) => { - if (!e.target.value) { - e.target.value = "1"; - } -};