diff --git a/index.html b/index.html index 0005b871..b5dc14ea 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,6 @@
- + diff --git a/package-lock.json b/package-lock.json index 8a563559..53466859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "devDependencies": { "@eslint/js": "^9.16.0", "@playwright/test": "^1.49.1", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.5.2", "@vitest/ui": "^2.1.8", @@ -21,24 +22,25 @@ "jsdom": "^25.0.1", "lint-staged": "^15.2.11", "prettier": "^3.4.2", + "typescript": "^5.7.2", "vite": "^6.0.3", "vitest": "^2.1.8" } }, "node_modules/@adobe/css-tools": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", - "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", + "integrity": "sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==", "dev": true }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, - "peer": true, "dependencies": { - "@babel/highlight": "^7.24.7", + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", "picocolors": "^1.0.0" }, "engines": { @@ -46,105 +48,19 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "peer": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "peer": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "peer": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "peer": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "peer": true - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "peer": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/runtime": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz", - "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", "dev": true, - "peer": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -637,9 +553,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", - "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.17.0.tgz", + "integrity": "sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -760,6 +676,24 @@ "node": ">=18" } }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -1018,7 +952,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -1038,7 +971,6 @@ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, - "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -1048,7 +980,6 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1064,8 +995,7 @@ "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@testing-library/jest-dom": { "version": "6.6.3", @@ -1104,8 +1034,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/estree": { "version": "1.0.6", @@ -1134,6 +1063,32 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", + "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.8.tgz", @@ -1282,13 +1237,15 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "peer": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -1313,9 +1270,9 @@ "dev": true }, "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "engines": { "node": ">= 0.4" @@ -1599,7 +1556,6 @@ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -1686,19 +1642,21 @@ } }, "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "peer": true, "engines": { - "node": ">=0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", - "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.17.0.tgz", + "integrity": "sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -1706,7 +1664,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.16.0", + "@eslint/js": "9.17.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -1715,7 +1673,7 @@ "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.5", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", @@ -1840,18 +1798,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", @@ -2056,9 +2002,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, "node_modules/form-data": { @@ -2076,9 +2022,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "optional": true, @@ -2333,8 +2279,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "peer": true + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -2596,7 +2541,6 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -2928,24 +2872,6 @@ "node": ">=0.10" } }, - "node_modules/playwright": { - "version": "1.49.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", - "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", - "dev": true, - "dependencies": { - "playwright-core": "1.49.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, "node_modules/playwright-core": { "version": "1.49.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", @@ -2958,20 +2884,6 @@ "node": ">=18" } }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/postcss": { "version": "8.4.49", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", @@ -3041,7 +2953,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -3051,12 +2962,20 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/pretty-format/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -3077,8 +2996,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/redent": { "version": "3.0.0", @@ -3097,8 +3015,7 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/resolve-from": { "version": "4.0.0", @@ -3351,18 +3268,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -3486,21 +3391,21 @@ } }, "node_modules/tldts": { - "version": "6.1.67", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.67.tgz", - "integrity": "sha512-714VbegxoZ9WF5/IsVCy9rWXKUpPkJq87ebWLXQzNawce96l5oRrRf2eHzB4pT2g/4HQU1dYbu+sdXClYxlDKQ==", + "version": "6.1.68", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.68.tgz", + "integrity": "sha512-JKF17jROiYkjJPT73hUTEiTp2OBCf+kAlB+1novk8i6Q6dWjHsgEjw9VLiipV4KTJavazXhY1QUXyQFSem2T7w==", "dev": true, "dependencies": { - "tldts-core": "^6.1.67" + "tldts-core": "^6.1.68" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.67", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.67.tgz", - "integrity": "sha512-12K5O4m3uUW6YM5v45Z7wc6NTSmAYj4Tq3de7eXghZkp879IlfPJrUWeWFwu1FS94U5t2vwETgJ1asu8UGNKVQ==", + "version": "6.1.68", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.68.tgz", + "integrity": "sha512-85TdlS/DLW/gVdf2oyyzqp3ocS30WxjaL4la85EArl9cHUR/nizifKAJPziWewSZjDZS71U517/i6ciUeqtB5Q==", "dev": true }, "node_modules/to-regex-range": { @@ -3566,6 +3471,19 @@ "node": ">= 0.8.0" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4074,6 +3992,20 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vite-node/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vite-node/node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", @@ -4133,6 +4065,20 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz", @@ -4566,32 +4512,6 @@ "node": ">=12" } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.8.tgz", - "integrity": "sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==", - "dev": true, - "dependencies": { - "@vitest/spy": "2.1.8", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "node_modules/vitest/node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -4630,6 +4550,20 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vitest/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest/node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", diff --git a/package.json b/package.json index e535bf69..e6477749 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@eslint/js": "^9.16.0", "@playwright/test": "^1.49.1", + "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.5.2", "@vitest/ui": "^2.1.8", @@ -40,6 +41,7 @@ "jsdom": "^25.0.1", "lint-staged": "^15.2.11", "prettier": "^3.4.2", + "typescript": "^5.7.2", "vite": "^6.0.3", "vitest": "^2.1.8" } diff --git a/src/__tests__/advanced.test.js b/src/__tests__/advanced.test.js index 9a507596..a00a99d8 100644 --- a/src/__tests__/advanced.test.js +++ b/src/__tests__/advanced.test.js @@ -13,7 +13,7 @@ beforeAll(async () => { // DOM 초기화 window.alert = vi.fn(); document.body.innerHTML = '
'; - await import("../main.js"); + await import("../main.ts"); }); afterAll(() => { diff --git a/src/__tests__/basic.test.js b/src/__tests__/basic.test.js index 20789e79..6aece5ac 100644 --- a/src/__tests__/basic.test.js +++ b/src/__tests__/basic.test.js @@ -14,7 +14,7 @@ beforeAll(async () => { // DOM 초기화 window.alert = vi.fn(); document.body.innerHTML = '
'; - await import("../main.js"); + await import("../main.ts"); }); afterAll(() => { diff --git a/src/components/footer/Footer.ts b/src/components/footer/Footer.ts new file mode 100644 index 00000000..b29f6965 --- /dev/null +++ b/src/components/footer/Footer.ts @@ -0,0 +1,7 @@ +export class Footer { + render() { + return ``; + } +} diff --git a/src/components/header/Header.ts b/src/components/header/Header.ts new file mode 100644 index 00000000..baa53e90 --- /dev/null +++ b/src/components/header/Header.ts @@ -0,0 +1,68 @@ +import { AuthentificatedNavigation } from "./../navigation/AuthentificatedNativation"; +import { UserStore } from "../../store/userStore"; +import { UserInfoType } from "../../utils/userPreference"; +import { UnauthentificatedNavigation } from "../navigation/UnauthenticatedNavigation"; +import { Router, Routes } from "../../router"; + +export class Header { + private static instance: Header | null = null; + private container!: HTMLElement; + authentificatedNavigation!: AuthentificatedNavigation; + unAuthentificatedNavigation!: UnauthentificatedNavigation; + userInfo!: UserInfoType | null; + + constructor(container: HTMLElement) { + if (Header.instance) { + return Header.instance; + } + + this.container = container; + + this.userInfo = UserStore.state.userInfo; + this.authentificatedNavigation = new AuthentificatedNavigation(); + this.unAuthentificatedNavigation = new UnauthentificatedNavigation(); + + this.attachEventListeners(); + + UserStore.addObserver({ + update: (state) => { + this.userInfo = state.userInfo; + this.container.innerHTML = this.render(); + }, + }); + + Header.instance = this; + } + + render() { + return ` +
+

항해플러스

+
+ ${this.userInfo ? this.authentificatedNavigation.render() : this.unAuthentificatedNavigation.render()} + `; + } + + attachEventListeners() { + this.container.addEventListener("click", this.handleClick); + } + + private handleClick = (event: MouseEvent) => { + if (event.target instanceof HTMLAnchorElement) { + event.preventDefault(); + + if (event.target.id === "logout") { + UserStore.actions.useLogoutUser(); + } + + const href = this.extractPathname(event.target.href); + + Router.push(href as Routes); + } + }; + + private extractPathname(url: string) { + const parsedUrl = new URL(url); + return parsedUrl.pathname; + } +} diff --git a/src/components/navigation/AuthentificatedNativation.ts b/src/components/navigation/AuthentificatedNativation.ts new file mode 100644 index 00000000..08375afb --- /dev/null +++ b/src/components/navigation/AuthentificatedNativation.ts @@ -0,0 +1,12 @@ +export class AuthentificatedNavigation { + render() { + return ` + `; + } +} diff --git a/src/components/navigation/UnauthenticatedNavigation.ts b/src/components/navigation/UnauthenticatedNavigation.ts new file mode 100644 index 00000000..70612963 --- /dev/null +++ b/src/components/navigation/UnauthenticatedNavigation.ts @@ -0,0 +1,12 @@ +export class UnauthentificatedNavigation { + render() { + return ` + + `; + } +} diff --git a/src/main.hash.js b/src/main.hash.js index 3ef381e0..d5b2e84e 100644 --- a/src/main.hash.js +++ b/src/main.hash.js @@ -1 +1 @@ -import "./main.js"; +import "./main.ts"; diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 036c2a38..00000000 --- a/src/main.js +++ /dev/null @@ -1,241 +0,0 @@ -const MainPage = () => ` -
-
-
-

항해플러스

-
- - - -
-
- - -
- -
- -
-
- 프로필 -
-

홍길동

-

5분 전

-
-
-

오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!

-
- - - -
-
- -
-
- 프로필 -
-

김철수

-

15분 전

-
-
-

새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!

-
- - - -
-
- -
-
- 프로필 -
-

이영희

-

30분 전

-
-
-

오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?

-
- - - -
-
- -
-
- 프로필 -
-

박민수

-

1시간 전

-
-
-

주말에 등산 가실 분 계신가요? 함께 가요!

-
- - - -
-
- -
-
- 프로필 -
-

정수연

-

2시간 전

-
-
-

새로 나온 영화 재미있대요. 같이 보러 갈 사람?

-
- - - -
-
-
-
- - -
-
-`; - -const ErrorPage = () => ` -
-
-

항해플러스

-

404

-

페이지를 찾을 수 없습니다

-

- 요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다. -

- - 홈으로 돌아가기 - -
-
-`; - -const LoginPage = () => ` -
-
-

항해플러스

-
-
- -
-
- -
- -
- -
-
- -
-
-
-`; - -const ProfilePage = () => ` -
-
-
-
-

항해플러스

-
- - - -
-
-

- 내 프로필 -

-
-
- - -
-
- - -
-
- - -
- -
-
-
- - -
-
-
-`; - -document.body.innerHTML = ` - ${MainPage()} - ${ProfilePage()} - ${LoginPage()} - ${ErrorPage()} -`; diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..8c5bc1d1 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,19 @@ +import { ErrorPage } from "./pages/error"; +import { LoginPage } from "./pages/login"; +import { MainPage } from "./pages/main"; +import { ProfilePage } from "./pages/profile"; +import { Router } from "./router"; + +const loginPage = new LoginPage(document.querySelector("#root")!); +const profilePage = new ProfilePage(document.querySelector("#root")!); +const mainPage = new MainPage(document.querySelector("#root")!); + +Router.createRoutes({ + route: { + "/": () => mainPage.render(), + "/profile": () => profilePage.render(), + "/login": () => loginPage.render(), + "404": ErrorPage, + }, + isHash: true, +}); diff --git a/src/pages/error/index.ts b/src/pages/error/index.ts new file mode 100644 index 00000000..a4dedeb3 --- /dev/null +++ b/src/pages/error/index.ts @@ -0,0 +1,15 @@ +export const ErrorPage = () => ` +
+
+

항해플러스

+

404

+

페이지를 찾을 수 없습니다

+

+ 요청하신 페이지가 존재하지 않거나 이동되었을 수 있습니다. +

+ + 홈으로 돌아가기 + +
+
+`; diff --git a/src/pages/login/index.ts b/src/pages/login/index.ts new file mode 100644 index 00000000..c202e91d --- /dev/null +++ b/src/pages/login/index.ts @@ -0,0 +1,58 @@ +import { UserStore } from "../../store/userStore"; +import { Router } from "../../router"; + +export class LoginPage { + private container: HTMLElement; + + constructor(container: HTMLElement) { + this.container = container; + this.attachEventListeners(); + } + + render() { + return ` +
+
+

항해플러스

+
+
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ `; + } + + attachEventListeners() { + this.container.addEventListener("submit", (event) => { + event.preventDefault(); + + if ( + event.target instanceof HTMLFormElement && + event.target.id === "login-form" + ) { + const username = event.target.querySelector( + "#username", + ) as HTMLInputElement; + + UserStore.actions.useSetUserInfo("username", username.value); + UserStore.actions.useSetUserInfo("email", ""); + UserStore.actions.useSetUserInfo("bio", ""); + + Router.push("/"); + } + }); + } +} diff --git a/src/pages/main/index.ts b/src/pages/main/index.ts new file mode 100644 index 00000000..f2248427 --- /dev/null +++ b/src/pages/main/index.ts @@ -0,0 +1,115 @@ +import { Footer } from "../../components/footer/Footer"; +import { Header } from "../../components/header/Header"; + +export class MainPage { + private container: HTMLElement; + private footer: Footer; + private header: Header; + + constructor(container: HTMLElement) { + this.container = container; + + this.footer = new Footer(); + this.header = new Header(this.container); + } + + render() { + return ` +
+
+
+ ${this.header.render()} +
+ + +
+ +
+ +
+
+ 프로필 +
+

홍길동

+

5분 전

+
+
+

오늘 날씨가 정말 좋네요. 다들 좋은 하루 보내세요!

+
+ + + +
+
+ +
+
+ 프로필 +
+

김철수

+

15분 전

+
+
+

새로운 프로젝트를 시작했어요. 열심히 코딩 중입니다!

+
+ + + +
+
+ +
+
+ 프로필 +
+

이영희

+

30분 전

+
+
+

오늘 점심 메뉴 추천 받습니다. 뭐가 좋을까요?

+
+ + + +
+
+ +
+
+ 프로필 +
+

박민수

+

1시간 전

+
+
+

주말에 등산 가실 분 계신가요? 함께 가요!

+
+ + + +
+
+ +
+
+ 프로필 +
+

정수연

+

2시간 전

+
+
+

새로 나온 영화 재미있대요. 같이 보러 갈 사람?

+
+ + + +
+
+
+
+ ${this.footer.render()} +
+
+`; + } +} diff --git a/src/pages/profile/index.ts b/src/pages/profile/index.ts new file mode 100644 index 00000000..c8dae95c --- /dev/null +++ b/src/pages/profile/index.ts @@ -0,0 +1,121 @@ +import { Footer } from "../../components/footer/Footer"; +import { Header } from "../../components/header/Header"; +import { UserStore } from "../../store/userStore"; +import { UserInfoType } from "../../utils/userPreference"; + +export class ProfilePage { + private container: HTMLElement; + private footer: Footer; + private header: Header; + private userInfo: UserInfoType | null; + + constructor(container: HTMLElement) { + this.container = container; + + this.footer = new Footer(); + this.header = new Header(this.container); + this.userInfo = UserStore.state.userInfo; + this.attachEventListeners(); + + UserStore.addObserver({ + update: (state) => { + this.userInfo = state.userInfo; + this.container.innerHTML = this.render(); + }, + }); + } + + render() { + return ` +
+
+ ${this.header.render()} +
+
+

+ 내 프로필 +

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ ${this.footer.render()} +
+
+ `; + } + + attachEventListeners() { + this.container.addEventListener("submit", this.handleSubmit); + } + + handleSubmit(event: SubmitEvent) { + event.preventDefault(); + + if ( + event.target instanceof HTMLFormElement && + event.target.id === "profile-form" + ) { + const username = event.target.querySelector( + "#username", + ) as HTMLInputElement; + + const email = event.target.querySelector("#email") as HTMLInputElement; + + const bio = event.target.querySelector("#bio") as HTMLInputElement; + + UserStore.actions.useSetAllUserInfo({ + username: username.value, + email: email.value, + bio: bio.value, + }); + } + } +} diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 00000000..69d6e35e --- /dev/null +++ b/src/router.ts @@ -0,0 +1,113 @@ +import { UserStore } from "./store/userStore"; + +export type Routes = "/" | "/profile" | "/login" | "404"; +type Component = () => string; + +interface createRoutesType { + route: Record; + isHash: boolean; +} + +export const Router = (function () { + const routes: Record = {} as Record; + + let routeMode: "hash" | "history"; + + function createRoutes(props: createRoutesType) { + (Object.keys(props.route) as (keyof typeof props.route)[]).forEach( + (prop) => { + routes[prop] = props.route[prop]; + }, + ); + + const { isHash } = props; + + if (isHash) { + attatchHistoryEvent(); + attatchHashEvent(); + } else { + attatchHistoryEvent(); + } + + routeMode = isHash && location.hash ? "hash" : "history"; + + handleRoute(); + } + + function push(path: Routes) { + const pathName = getMappingPathByRoutingMode(path); + history.pushState({}, "", pathName); + + handleRoute(); + } + + function replace(path: Routes) { + const pathName = getMappingPathByRoutingMode(path); + + history.replaceState({}, "", pathName); + + handleRoute(); + } + + function getMappingPathByRoutingMode(path: Routes) { + if (location.hash) { + return `#${path}`; + } else { + return path; + } + } + + function handleRoute() { + const path = + routeMode === "history" + ? (location.pathname as Routes) + : (location.hash.replace("#", "") as Routes); + + const redirectPath = routeGuard(path); + const shouldRedirect = !!redirectPath; + + if (shouldRedirect && redirectPath !== path) { + push(redirectPath); + return; + } + + const component = routes[path] || routes["404"]; + + const root = document.getElementById("root"); + + if (root) { + root.innerHTML = component(); + } + } + + function routeGuard(path: Routes): Routes | undefined { + switch (path) { + case "/login": + return UserStore.state.userInfo ? "/" : undefined; + case "/profile": + return !UserStore.state.userInfo ? "/login" : undefined; + default: + return undefined; + } + } + + function attatchHistoryEvent() { + window.addEventListener("popstate", () => { + routeMode = "history"; + handleRoute(); + }); + } + + function attatchHashEvent() { + window.addEventListener("hashchange", () => { + routeMode = "hash"; + handleRoute(); + }); + } + + return { + replace, + push, + createRoutes, + }; +})(); diff --git a/src/store/userStore.ts b/src/store/userStore.ts new file mode 100644 index 00000000..076ddb30 --- /dev/null +++ b/src/store/userStore.ts @@ -0,0 +1,67 @@ +import { + UserInfoKeys, + UserInfoType, + userPreference, +} from "../utils/userPreference"; + +interface Observer { + update(state: StateTypes): void; +} + +interface StateTypes { + userInfo: UserInfoType | null; +} + +interface ActionTypes { + useGetUserInfo: () => UserInfoType | null; + useSetUserInfo: (key: UserInfoKeys, value: string) => void; + useSetAllUserInfo: (value: UserInfoType) => void; + useLogoutUser: () => void; +} + +export const UserStore = (function () { + const observers: Observer[] = []; + + const state: StateTypes = { + userInfo: userPreference.getAll(), + }; + + function notifyObservers() { + observers.forEach((observer) => observer.update(state)); + } + + const actions: ActionTypes = { + useGetUserInfo: () => { + return state.userInfo; + }, + useSetUserInfo: (key: UserInfoKeys, value: string) => { + userPreference.set(key, value); + state.userInfo = userPreference.getAll(); + notifyObservers(); + }, + useSetAllUserInfo: (value: UserInfoType) => { + userPreference.setAll(value); + state.userInfo = value; + notifyObservers(); + }, + useLogoutUser: () => { + userPreference.remove(); + state.userInfo = null; + notifyObservers(); + }, + }; + + return { + actions, + state, + addObserver: (observer: Observer) => { + observers.push(observer); + }, + removeObserver: (observer: Observer) => { + const index = observers.indexOf(observer); + if (index > -1) { + observers.splice(index, 1); + } + }, + }; +})(); diff --git a/src/utils/userPreference.ts b/src/utils/userPreference.ts new file mode 100644 index 00000000..a56c60d6 --- /dev/null +++ b/src/utils/userPreference.ts @@ -0,0 +1,56 @@ +export interface UserInfoType { + username: string; + email?: string | number; + bio?: string; +} + +export type UserInfoKeys = keyof UserInfoType; + +const USER_STORAGE_KEY = "user"; + +export const userPreference = (function () { + let userStorage: UserInfoType | null = init(); + + function get(key: UserInfoKeys) { + return userStorage?.[key] ?? null; + } + + function getAll() { + return userStorage; + } + + function set(key: UserInfoKeys, value: string) { + if (!userStorage) { + userStorage = {} as UserInfoType; + } + + userStorage[key] = value; + save(); + } + + function setAll(value: UserInfoType | null) { + const newUserStorage = value; + + userStorage = newUserStorage; + + save(); + } + + function remove() { + localStorage.removeItem(USER_STORAGE_KEY); + userStorage = null; + } + + function save() { + if (userStorage) { + localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(userStorage)); + } + } + + function init() { + const user = localStorage.getItem(USER_STORAGE_KEY); + return user ? (JSON.parse(user) as UserInfoType) : null; + } + + return { get, getAll, set, setAll, save, remove }; +})(); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..a709e70d --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "strict": true, + "declaration": true, + "sourceMap": true, + "moduleResolution": "node", + "typeRoots": ["./node_modules/@types"], + "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitAny": true, + "outDir": "./dist", + "rootDir": "./src" + } +}