diff --git a/frontend/.gitignore b/frontend/.gitignore index a660195f7..764ced914 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -16,4 +16,6 @@ build-storybook.log *.csr *storybook.log -coverage/ \ No newline at end of file +coverage/ +# Sentry Config File +.env.sentry-build-plugin diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e58c4e009..5a4edb8cb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,8 @@ "license": "ISC", "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", + "@sentry/react": "^9.18.0", + "@sentry/webpack-plugin": "^3.4.0", "@tanstack/react-query": "^5.66.0", "date-fns": "^4.1.0", "dotenv-webpack": "^8.1.0", @@ -89,7 +91,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -103,7 +104,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -118,7 +118,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -128,7 +127,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -159,7 +157,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.26.5", @@ -176,7 +173,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.26.5", @@ -193,7 +189,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -207,7 +202,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.25.9", @@ -235,7 +229,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -245,7 +238,6 @@ "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, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -255,7 +247,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -265,7 +256,6 @@ "version": "7.26.0", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.25.9", @@ -279,7 +269,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.5" @@ -547,7 +536,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.25.9", @@ -562,7 +550,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -581,7 +568,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -591,7 +577,6 @@ "version": "7.26.5", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -2060,6 +2045,451 @@ "integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==", "license": "MIT" }, + "node_modules/@sentry-internal/browser-utils": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz", + "integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz", + "integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz", + "integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "9.18.0", + "@sentry/core": "9.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz", + "integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "9.18.0", + "@sentry/core": "9.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz", + "integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz", + "integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "9.18.0", + "@sentry-internal/feedback": "9.18.0", + "@sentry-internal/replay": "9.18.0", + "@sentry-internal/replay-canvas": "9.18.0", + "@sentry/core": "9.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/bundler-plugin-core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz", + "integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "3.4.0", + "@sentry/cli": "2.42.2", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^9.3.2", + "magic-string": "0.30.8", + "unplugin": "1.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "license": "MIT" + }, + "node_modules/@sentry/cli": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz", + "integrity": "sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.42.2", + "@sentry/cli-linux-arm": "2.42.2", + "@sentry/cli-linux-arm64": "2.42.2", + "@sentry/cli-linux-i686": "2.42.2", + "@sentry/cli-linux-x64": "2.42.2", + "@sentry/cli-win32-i686": "2.42.2", + "@sentry/cli-win32-x64": "2.42.2" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz", + "integrity": "sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz", + "integrity": "sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz", + "integrity": "sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz", + "integrity": "sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz", + "integrity": "sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz", + "integrity": "sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz", + "integrity": "sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz", + "integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz", + "integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "9.18.0", + "@sentry/core": "9.18.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/webpack-plugin": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz", + "integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "3.4.0", + "unplugin": "1.0.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "webpack": ">=4.40.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -4932,7 +5362,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -5090,7 +5519,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -5443,7 +5871,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-arraybuffer": { @@ -5546,7 +5973,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5630,7 +6056,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5986,7 +6411,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -6303,7 +6727,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -7422,12 +7845,15 @@ } }, "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { - "node": ">=10" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-defaults": { @@ -7439,6 +7865,15 @@ "dotenv": "^8.2.0" } }, + "node_modules/dotenv-defaults/node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, "node_modules/dotenv-webpack": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.1.0.tgz", @@ -9075,7 +9510,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -9331,14 +9765,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "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, "license": "MIT", "optional": true, @@ -9394,7 +9826,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -9529,7 +9960,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -9879,6 +10309,15 @@ "he": "bin/he" } }, + "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", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", @@ -10157,7 +10596,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -10455,7 +10893,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -10575,7 +11012,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10640,7 +11076,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -10708,7 +11143,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -10949,7 +11383,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isobject": { @@ -12177,7 +12610,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -12290,7 +12722,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -12329,7 +12760,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -12561,7 +12991,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -13751,6 +14180,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -13849,7 +14287,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -13893,7 +14330,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14349,7 +14785,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14382,6 +14817,37 @@ "dev": true, "license": "MIT" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/path-to-regexp": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", @@ -14436,7 +14902,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -15199,6 +15664,15 @@ "dev": true, "license": "MIT" }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/promise-polyfill": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", @@ -15266,6 +15740,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -15485,7 +15965,6 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, "license": "MIT" }, "node_modules/react-markdown": { @@ -15583,7 +16062,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -16176,7 +16654,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -17462,7 +17939,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -17511,7 +17987,6 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/tree-dump": { @@ -18274,7 +18749,6 @@ "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, "license": "BSD-2-Clause" }, "node_modules/webpack": { @@ -18685,7 +19159,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -18696,7 +19169,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -18912,7 +19384,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -18958,7 +19429,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/frontend/package.json b/frontend/package.json index 1adda2f86..40059cf38 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,8 @@ "description": "", "dependencies": { "@channel.io/channel-web-sdk-loader": "^2.0.0", + "@sentry/react": "^9.18.0", + "@sentry/webpack-plugin": "^3.4.0", "@tanstack/react-query": "^5.66.0", "date-fns": "^4.1.0", "dotenv-webpack": "^8.1.0", diff --git a/frontend/src/assets/images/icons/sns/instagram_icon.png b/frontend/src/assets/images/icons/sns/instagram_icon.png new file mode 100644 index 000000000..87bf66c30 Binary files /dev/null and b/frontend/src/assets/images/icons/sns/instagram_icon.png differ diff --git a/frontend/src/assets/images/icons/sns/x_icon.svg b/frontend/src/assets/images/icons/sns/x_icon.svg new file mode 100644 index 000000000..56368dd4b --- /dev/null +++ b/frontend/src/assets/images/icons/sns/x_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/icons/sns/youtube_icon.svg b/frontend/src/assets/images/icons/sns/youtube_icon.svg new file mode 100644 index 000000000..0b6492b62 --- /dev/null +++ b/frontend/src/assets/images/icons/sns/youtube_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx index 94037751a..e1e4c3d8c 100644 --- a/frontend/src/components/common/Button/Button.tsx +++ b/frontend/src/components/common/Button/Button.tsx @@ -4,7 +4,8 @@ import styled, { keyframes, css } from 'styled-components'; export interface ButtonProps { width?: string; children: React.ReactNode; - onClick: () => void; + type?: string; + onClick?: () => void; animated?: boolean; } @@ -44,9 +45,10 @@ const Button = ({ width, children, onClick, + type, animated = false, }: ButtonProps) => ( - + {children} ); diff --git a/frontend/src/components/common/InputField/InputField.styles.ts b/frontend/src/components/common/InputField/InputField.styles.ts index 8cebe69e1..42dc0655c 100644 --- a/frontend/src/components/common/InputField/InputField.styles.ts +++ b/frontend/src/components/common/InputField/InputField.styles.ts @@ -19,20 +19,22 @@ export const InputWrapper = styled.div` align-items: center; `; -export const Input = styled.input` +export const Input = styled.input<{ hasError?: boolean }>` flex: 1; height: 45px; padding: 12px 80px 12px 18px; - border: 1px solid #c5c5c5; + border: 1px solid ${({ hasError }) => (hasError ? 'red' : '#c5c5c5')}; border-radius: 6px; outline: none; font-size: 1.125rem; letter-spacing: 0; - color: rgba(0, 0, 0, 0.5); + color: rgba(0, 0, 0, 0.8); &:focus { - border-color: #007bff; - box-shadow: 0 0 3px rgba(0, 123, 255, 0.5); + border-color: ${({ hasError }) => (hasError ? 'red' : '#007bff')}; + box-shadow: 0 0 3px + ${({ hasError }) => + hasError ? 'rgba(255, 0, 0, 0.5)' : 'rgba(0, 123, 255, 0.5)'}; } ${({ disabled }) => @@ -40,6 +42,9 @@ export const Input = styled.input` ` background-color: rgba(0, 0, 0, 0.05); `} + &::placeholder { + color: rgba(0, 0, 0, 0.3); + } `; export const ClearButton = styled.button` @@ -85,3 +90,15 @@ export const CharCount = styled.span` font-size: 12px; letter-spacing: -0.96px; `; + +export const HelperText = styled.div` + position: absolute; + left: 0; + top: 100%; + font-size: 0.75rem; + color: red; + margin-top: 4px; + pointer-events: none; + white-space: nowrap; + z-index: 1; +`; diff --git a/frontend/src/components/common/InputField/InputField.tsx b/frontend/src/components/common/InputField/InputField.tsx index b2a843744..16e507cde 100644 --- a/frontend/src/components/common/InputField/InputField.tsx +++ b/frontend/src/components/common/InputField/InputField.tsx @@ -14,6 +14,8 @@ interface CustomInputProps { value?: string; onChange?: (e: React.ChangeEvent) => void; onClear?: () => void; + isError?: boolean; + helperText?: string; } const InputField = ({ @@ -28,6 +30,8 @@ const InputField = ({ value = '', onChange, onClear, + isError, + helperText, }: CustomInputProps) => { const [isPasswordVisible, setIsPasswordVisible] = useState(false); @@ -64,6 +68,7 @@ const InputField = ({ placeholder={placeholder} maxLength={maxLength} disabled={disabled} + hasError={isError} /> {showClearButton && !disabled && ( @@ -81,6 +86,9 @@ const InputField = ({ )} + {isError && helperText && ( + {helperText} + )} ); }; diff --git a/frontend/src/constants/snsConfig.ts b/frontend/src/constants/snsConfig.ts new file mode 100644 index 000000000..2d85fcd17 --- /dev/null +++ b/frontend/src/constants/snsConfig.ts @@ -0,0 +1,24 @@ +import youtube_icon from '@/assets/images/icons/sns/youtube_icon.svg'; +import instagram_icon from '@/assets/images/icons/sns/instagram_icon.png'; +import x_icon from '@/assets/images/icons/sns/x_icon.svg'; + +export const SNS_CONFIG = { + instagram: { + label: '인스타그램', + placeholder: 'https://www.instagram.com/id', + regex: /^https:\/\/(www\.)?instagram\.com\/[A-Za-z0-9._%-]+\/?$/, + icon: instagram_icon, + }, + youtube: { + label: '유튜브', + placeholder: 'https://www.youtube.com/@id', + regex: /^https:\/\/(www\.)?youtube\.com\/(channel\/|@)[A-Za-z0-9._%-]+\/?$/, + icon: youtube_icon, + }, + x: { + label: 'X', + placeholder: 'https://x.com/id', + regex: /^https:\/\/(www\.)?x\.com\/[A-Za-z0-9._%-]+\/?$/, + icon: x_icon, + }, +} as const; diff --git a/frontend/src/hooks/useTrackPageView.ts b/frontend/src/hooks/useTrackPageView.ts index 42ac533b7..b64a7ddbb 100644 --- a/frontend/src/hooks/useTrackPageView.ts +++ b/frontend/src/hooks/useTrackPageView.ts @@ -1,33 +1,33 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import mixpanel from 'mixpanel-browser'; -const useTrackPageView = (pageName: string) => { +const useTrackPageView = (pageName: string, clubName?: string) => { const location = useLocation(); + const isTracked = useRef(false); + const startTime = useRef(Date.now()); useEffect(() => { - const startTime = Date.now(); - - // 페이지 방문 이벤트 mixpanel.track(`${pageName} Visited`, { url: window.location.href, - timestamp: startTime, + timestamp: startTime.current, referrer: document.referrer || 'direct', + clubName, }); const trackPageDuration = () => { - const duration = Date.now() - startTime; + if (isTracked.current) return; + const duration = Date.now() - startTime.current; mixpanel.track(`${pageName} Duration`, { url: window.location.href, - duration: duration, // milliseconds - duration_seconds: Math.round(duration / 1000), // Convert to seconds + duration: duration, + duration_seconds: Math.round(duration / 1000), + clubName, }); + isTracked.current = true; }; - // 사용자가 페이지를 떠날 때 (페이지 종료 또는 새 페이지 이동) window.addEventListener('beforeunload', trackPageDuration); - - // 사용자가 탭을 변경하거나 백그라운드로 이동할 때 document.addEventListener('visibilitychange', () => { if (document.hidden) { trackPageDuration(); @@ -35,10 +35,11 @@ const useTrackPageView = (pageName: string) => { }); return () => { + trackPageDuration(); window.removeEventListener('beforeunload', trackPageDuration); document.removeEventListener('visibilitychange', trackPageDuration); }; - }, [location.pathname]); + }, [location.pathname, clubName]); }; export default useTrackPageView; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 6e2cf0baf..20342c892 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'; import App from './App'; import mixpanel from 'mixpanel-browser'; import * as ChannelService from '@channel.io/channel-web-sdk-loader'; +import * as Sentry from '@sentry/react'; if (process.env.REACT_APP_MIXPANEL_TOKEN) { mixpanel.init(process.env.REACT_APP_MIXPANEL_TOKEN, { @@ -22,6 +23,13 @@ if (process.env.CHANNEL_PLUGIN_KEY) { }); } +Sentry.init({ + dsn: process.env.SENTRY_DSN, + sendDefaultPii: false, + release: process.env.SENTRY_RELEASE, + tracesSampleRate: 0.1, +}); + const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, ); diff --git a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts index f142a2a4b..564715a50 100644 --- a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts +++ b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.styles.ts @@ -36,6 +36,12 @@ export const Title = styled.h2` align-self: flex-start; `; +export const LoginForm = styled.form` + width: 100%; + display: flex; + flex-direction: column; +`; + export const InputFieldsContainer = styled.div` width: 100%; display: flex; diff --git a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx index 4b5541b89..bfb31901a 100644 --- a/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx +++ b/frontend/src/pages/AdminPage/auth/LoginTab/LoginTab.tsx @@ -49,28 +49,35 @@ const LoginTab = () => { Log in - - setUserId(e.target.value)} - /> - setPassword(e.target.value)} - /> - + { + e.preventDefault(); + handleLogin(); + }} + > + + setUserId(e.target.value)} + /> + setPassword(e.target.value)} + /> + - - - + + + + { alert( '해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺', ) - }> + } + > 회원가입 | @@ -89,7 +97,8 @@ const LoginTab = () => { alert( '해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺', ) - }> + } + > 아이디 찾기 | @@ -99,7 +108,8 @@ const LoginTab = () => { alert( '해당 기능은 아직 준비 중이에요.\n필요하신 경우 관리자에게 문의해주세요☺', ) - }> + } + > 비밀번호 찾기 diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts index 7177019ad..645d6a935 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.styles.ts @@ -31,4 +31,27 @@ export const TagEditGroup = styled.div` display: flex; flex-direction: column; gap: 30px; + margin-bottom: 120px; +`; + +export const SNSInputGroup = styled.div` + display: flex; + flex-direction: column; + gap: 30px; + margin-top: 30px; +`; + +export const SNSRow = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + position: relative; + align-items: flex-start; +`; + +export const SNSCheckboxLabel = styled.label` + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; `; diff --git a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx index c3557f364..175970aac 100644 --- a/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/ClubInfoEditTab.tsx @@ -1,13 +1,16 @@ import React, { useState, useEffect } from 'react'; -import * as Styled from './ClubInfoEditTab.styles'; -import InputField from '@/components/common/InputField/InputField'; -import SelectTags from '@/pages/AdminPage/components/SelectTags/SelectTags'; -import MakeTags from '@/pages/AdminPage/components/MakeTags/MakeTags'; -import Button from '@/components/common/Button/Button'; import { useOutletContext } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; import { ClubDetail } from '@/types/club'; +import { SNSPlatform } from '@/types/club'; import { useUpdateClubDetail } from '@/hooks/queries/club/useUpdateClubDetail'; -import { useQueryClient } from '@tanstack/react-query'; +import { validateSocialLink } from '@/utils/validateSocialLink'; +import { SNS_CONFIG } from '@/constants/snsConfig'; +import InputField from '@/components/common/InputField/InputField'; +import Button from '@/components/common/Button/Button'; +import SelectTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags'; +import MakeTags from '@/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags'; +import * as Styled from './ClubInfoEditTab.styles'; const ClubInfoEditTab = () => { const clubDetail = useOutletContext(); @@ -20,7 +23,18 @@ const ClubInfoEditTab = () => { const [selectedDivision, setSelectedDivision] = useState(''); const [selectedCategory, setSelectedCategory] = useState(''); const [clubTags, setClubTags] = useState(() => ['', '']); - const [recruitmentForm] = useState(''); + const [socialLinks, setSocialLinks] = useState>({ + instagram: '', + youtube: '', + x: '', + }); + + const [snsErrors, setSnsErrors] = useState>({ + instagram: '', + youtube: '', + x: '', + }); + const queryClient = useQueryClient(); const divisions = ['중동', '과동']; const categories = ['봉사', '종교', '취미교양', '학술', '운동', '공연']; @@ -39,8 +53,21 @@ const ClubInfoEditTab = () => { : [...clubDetail.tags, ''], ); } + if (clubDetail?.socialLinks) { + setSocialLinks({ + instagram: clubDetail.socialLinks.instagram || '', + youtube: clubDetail.socialLinks.youtube || '', + x: clubDetail.socialLinks.x || '', + }); + } }, [clubDetail]); + const handleSocialLinkChange = (key: SNSPlatform, value: string) => { + const errorMsg = validateSocialLink(key, value); + setSnsErrors((prev) => ({ ...prev, [key]: errorMsg })); + setSocialLinks((prev) => ({ ...prev, [key]: value })); + }; + const handleUpdateClub = () => { if (!clubDetail || !clubDetail.id) { alert('클럽 정보가 로드되지 않았습니다.'); @@ -51,6 +78,12 @@ const ClubInfoEditTab = () => { return; } + const hasSnsErrors = Object.values(snsErrors).some((error) => error !== ''); + if (hasSnsErrors) { + alert('SNS 링크에 오류가 있어요. 수정 후 다시 시도해주세요!'); + return; + } + const updatedData = { id: clubDetail.id, name: clubName, @@ -60,7 +93,7 @@ const ClubInfoEditTab = () => { introduction: introduction, presidentName: clubPresidentName, presidentPhoneNumber: telephoneNumber, - recruitmentForm: recruitmentForm, + socialLinks: socialLinks, }; updateClub(updatedData, { @@ -149,6 +182,30 @@ const ClubInfoEditTab = () => { + + 동아리 SNS 링크 + + {Object.entries(SNS_CONFIG).map(([rawKey, { label, placeholder }]) => { + const key = rawKey as SNSPlatform; + + return ( + + {label} + handleSocialLinkChange(key, e.target.value)} + onClear={() => { + setSocialLinks((prev) => ({ ...prev, [key]: '' })); + setSnsErrors((prev) => ({ ...prev, [key]: '' })); + }} + isError={snsErrors[key] !== ''} + helperText={snsErrors[key]} + /> + + ); + })} + ); }; diff --git a/frontend/src/pages/AdminPage/components/MakeTags/MakeTags.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/MakeTags/MakeTags.styles.ts rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.styles.ts diff --git a/frontend/src/pages/AdminPage/components/MakeTags/MakeTags.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx similarity index 100% rename from frontend/src/pages/AdminPage/components/MakeTags/MakeTags.tsx rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/MakeTags/MakeTags.tsx diff --git a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.styles.ts b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/SelectTags/SelectTags.styles.ts rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.styles.ts diff --git a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx similarity index 92% rename from frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx rename to frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx index 435bfb865..57b98c94a 100644 --- a/frontend/src/pages/AdminPage/components/SelectTags/SelectTags.tsx +++ b/frontend/src/pages/AdminPage/tabs/ClubInfoEditTab/components/SelectTags/SelectTags.tsx @@ -17,7 +17,8 @@ const SelectTags = ({ label, tags, selected, onChange }: SelectTagsProps) => { onChange(tag)} - selected={selected === tag}> + selected={selected === tag} + > #{tag} ))} diff --git a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx index 4b1663eed..883b89ac2 100644 --- a/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/PhotoEditTab.tsx @@ -1,8 +1,8 @@ import React, { useEffect, useState } from 'react'; import { useOutletContext } from 'react-router-dom'; import * as Styled from './PhotoEditTab.styles'; -import ImageUpload from '@/pages/AdminPage/components/ImageUpload/ImageUpload'; -import { ImagePreview } from '@/pages/AdminPage/components/ImagePreview/ImagePreview'; +import ImageUpload from '@/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload'; +import { ImagePreview } from '@/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview'; import useUpdateFeedImages from '@/hooks/queries/club/useUpdateFeedImages'; import { ClubDetail } from '@/types/club'; diff --git a/frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.styles.ts b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.styles.ts rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.styles.ts diff --git a/frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx similarity index 100% rename from frontend/src/pages/AdminPage/components/ImagePreview/ImagePreview.tsx rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImagePreview/ImagePreview.tsx diff --git a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.styles.ts b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.styles.ts rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.styles.ts diff --git a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx similarity index 95% rename from frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx rename to frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx index 3fc1a8aff..57379f4c9 100644 --- a/frontend/src/pages/AdminPage/components/ImageUpload/ImageUpload.tsx +++ b/frontend/src/pages/AdminPage/tabs/PhotoEditTab/components/ImageUpload/ImageUpload.tsx @@ -1,6 +1,5 @@ import React, { useRef } from 'react'; import * as Styled from './ImageUpload.styles'; -// import UploadAddIcon from '@/assets/images/upload-add.png'; import useCreateFeedImage from '@/hooks/queries/club/useCreateFeedImage'; import Button from '@/components/common/Button/Button'; import { MAX_FILE_SIZE } from '@/constants/uploadLimit'; @@ -52,7 +51,8 @@ export const ImageUpload = ({ return; } inputRef.current?.click(); - }}> + }} + > 이미지 업로드 {/*Upload Add Icon*/} diff --git a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx index 4cc3792fd..d3d1fa353 100644 --- a/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx +++ b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/RecruitEditTab.tsx @@ -1,14 +1,14 @@ import React, { useState, useEffect } from 'react'; import { useOutletContext } from 'react-router-dom'; import * as Styled from './RecruitEditTab.styles'; -import Calendar from '@/pages/AdminPage/components/Calendar/Calendar'; +import Calendar from '@/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar'; import Button from '@/components/common/Button/Button'; import InputField from '@/components/common/InputField/InputField'; import { useUpdateClubDescription } from '@/hooks/queries/club/useUpdateClubDescription'; import { parseRecruitmentPeriod } from '@/utils/stringToDate'; import { ClubDetail } from '@/types/club'; import { useQueryClient } from '@tanstack/react-query'; -import MarkdownEditor from '@/pages/AdminPage/components/MarkdownEditor/MarkdownEditor'; +import MarkdownEditor from '@/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor'; const RecruitEditTab = () => { const clubDetail = useOutletContext(); diff --git a/frontend/src/pages/AdminPage/components/Calendar/Calendar.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/Calendar/Calendar.styles.ts rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.styles.ts diff --git a/frontend/src/pages/AdminPage/components/Calendar/Calendar.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx similarity index 100% rename from frontend/src/pages/AdminPage/components/Calendar/Calendar.tsx rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/Calendar/Calendar.tsx diff --git a/frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.styles.ts b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.styles.ts similarity index 100% rename from frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.styles.ts rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.styles.ts diff --git a/frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.tsx b/frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.tsx similarity index 100% rename from frontend/src/pages/AdminPage/components/MarkdownEditor/MarkdownEditor.tsx rename to frontend/src/pages/AdminPage/tabs/RecruitEditTab/components/MarkdownEditor/MarkdownEditor.tsx diff --git a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx index 97be1498a..84938ef74 100644 --- a/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx +++ b/frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx @@ -30,7 +30,7 @@ const ClubDetailPage = () => { return () => window.removeEventListener('resize', handleResize); }, []); - useTrackPageView(`ClubDetailPage ${clubDetail?.name || ''}`); + useTrackPageView(`ClubDetailPage`, clubDetail?.name); if (!clubDetail) { return null; diff --git a/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx b/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx index 143436e5d..1608a26bf 100644 --- a/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx +++ b/frontend/src/pages/ClubDetailPage/components/ClubProfile/ClubProfile.tsx @@ -25,13 +25,19 @@ const ClubProfile = ({ {name} - {division} - {category} - {tags.map((tag) => ( - - {tag} - - ))} + + {division} + + + {category} + + {tags + .filter((tag) => tag.trim()) + .map((tag) => ( + + {tag} + + ))} diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts index d4f9b1d99..d7f65efac 100644 --- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts +++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.styles.ts @@ -9,14 +9,14 @@ export const InfoBoxWrapper = styled.div` @media (max-width: 500px) { flex-direction: column; - gap: 0px; + gap: 0; margin-top: 40px; } `; export const InfoBox = styled.div` width: 573px; - height: 164px; + height: 197px; border-radius: 18px; border: 1px solid #dcdcdc; padding: 30px; @@ -52,11 +52,15 @@ export const DescriptionWrapper = styled.div` align-items: center; gap: 50px; font-size: 14px; + @media (max-width: 500px) { + gap: 40px; + } `; export const LeftText = styled.p` color: #9d9d9d; white-space: nowrap; + width: 30px; `; export const RightText = styled.p` diff --git a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx index 5ffdc0a2c..0b0f7cf12 100644 --- a/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx +++ b/frontend/src/pages/ClubDetailPage/components/InfoBox/InfoBox.tsx @@ -2,10 +2,12 @@ import React from 'react'; import * as Styled from './InfoBox.styles'; import { ClubDetail } from '@/types/club'; import { INFOTABS_SCROLL_INDEX } from '@/constants/scrollSections'; +import SnsLinkIcons from '@/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons'; interface ClubInfoItem { label: string; - value: string; + value?: string; + render?: React.ReactNode; } interface ClubInfoSection { @@ -37,6 +39,10 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => { descriptions: [ { label: '회장이름', value: clubDetail.presidentName }, { label: '전화번호', value: clubDetail.presidentPhoneNumber }, + { + label: 'SNS', + render: , + }, ], refIndex: INFOTABS_SCROLL_INDEX.CLUB_INFO_TAB, }, @@ -49,13 +55,14 @@ const InfoBox = ({ sectionRefs, clubDetail }: InfoBoxProps) => { key={info.refIndex} ref={(el) => { sectionRefs.current[info.refIndex] = el; - }}> + }} + > {info.title} {info.descriptions.map((desc, idx) => ( {desc.label} - {desc.value} + {desc.render ?? desc.value} ))} diff --git a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts new file mode 100644 index 000000000..f2b70da71 --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.styles.ts @@ -0,0 +1,25 @@ +import styled from 'styled-components'; + +export const SnsIconGroup = styled.div` + display: flex; + gap: 8px; + align-items: center; +`; + +export const SnsIcon = styled.img` + width: 24px; + height: 24px; + cursor: pointer; + opacity: 0.9; + &:hover { + opacity: 1; + } + &:active { + transform: scale(0.98); + } +`; + +export const SnsLink = styled.a` + display: inline-block; + line-height: 0; +`; diff --git a/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx new file mode 100644 index 000000000..3b5716d47 --- /dev/null +++ b/frontend/src/pages/ClubDetailPage/components/SnsLinkIcons/SnsLinkIcons.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import * as Styled from './SnsLinkIcons.styles'; +import { SNS_CONFIG } from '@/constants/snsConfig'; +import { SNSPlatform } from '@/types/club'; + +interface SnsLinkIconsProps { + apiSocialLinks: Partial>; +} + +const SnsLinkIcons = ({ apiSocialLinks }: SnsLinkIconsProps) => { + return ( + + {Object.entries(apiSocialLinks).map(([platform, url]) => { + if (!url) return null; + + const config = SNS_CONFIG[platform as SNSPlatform]; + return ( + + + + ); + })} + + ); +}; + +export default SnsLinkIcons; diff --git a/frontend/src/pages/MainPage/components/Banner/Banner.tsx b/frontend/src/pages/MainPage/components/Banner/Banner.tsx index 39668bba3..1bd1ed406 100644 --- a/frontend/src/pages/MainPage/components/Banner/Banner.tsx +++ b/frontend/src/pages/MainPage/components/Banner/Banner.tsx @@ -17,23 +17,33 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => { const [isMobile, setIsMobile] = useState(window.innerWidth <= 500); const [currentSlideIndex, setCurrentSlideIndex] = useState(1); const [slideWidth, setSlideWidth] = useState(0); - const [isAnimating, setIsAnimating] = useState(true); + const [isAnimating, setIsAnimating] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false); + const [isReady, setIsReady] = useState(false); const banners = isMobile ? mobileBanners : desktopBanners; const extendedBanners = [banners[banners.length - 1], ...banners, banners[0]]; const updateSlideWidth = useCallback(() => { if (slideRef.current) { - setSlideWidth(slideRef.current.offsetWidth); - slideRef.current.style.transform = `translateX(-${currentSlideIndex * slideRef.current.offsetWidth}px)`; + const width = slideRef.current.offsetWidth; + setSlideWidth(width); + if (width > 0) { + slideRef.current.style.transform = `translateX(-${currentSlideIndex * width}px)`; + if (!isReady) { + setIsReady(true); + setIsAnimating(true); + } + } } - }, [currentSlideIndex]); + }, [currentSlideIndex, isReady]); useEffect(() => { updateSlideWidth(); const handleResize = debounce(() => { setIsMobile(window.innerWidth <= 500); + setIsReady(false); + setIsAnimating(false); updateSlideWidth(); }, 200); @@ -44,7 +54,7 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => { }, [updateSlideWidth]); useEffect(() => { - if (!slideRef.current) return; + if (!slideRef.current || slideWidth === 0) return; if (isAnimating) { slideRef.current.style.transform = `translateX(-${currentSlideIndex * slideWidth}px)`; @@ -77,27 +87,28 @@ const Banner = ({ desktopBanners, mobileBanners }: BannerComponentProps) => { }, [currentSlideIndex, slideWidth, banners.length, isAnimating]); const moveToNextSlide = useCallback(() => { - if (isTransitioning) return; + if (isTransitioning || !isReady) return; setIsTransitioning(true); setIsAnimating(true); setCurrentSlideIndex((prev) => prev + 1); - }, [isTransitioning]); + }, [isTransitioning, isReady]); const moveToPrevSlide = useCallback(() => { - if (isTransitioning) return; + if (isTransitioning || !isReady) return; setIsTransitioning(true); setIsAnimating(true); setCurrentSlideIndex((prev) => prev - 1); - }, [isTransitioning]); + }, [isTransitioning, isReady]); useEffect(() => { - if (slideWidth === 0) return; + if (slideWidth === 0 || !isReady) return; + const interval = setInterval(() => { moveToNextSlide(); }, 3000); return () => clearInterval(interval); - }, [moveToNextSlide, slideWidth]); + }, [moveToNextSlide, slideWidth, isReady]); return ( diff --git a/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx b/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx index 5443e709d..78c400bc0 100644 --- a/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx +++ b/frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx @@ -40,13 +40,15 @@ const ClubCard = ({ club }: { club: Club }) => { {club.introduction} - - - {club.tags.map((tag) => ( - - {tag} - - ))} + + + {club.tags + .filter((tag) => tag.trim()) + .map((tag) => ( + + {tag} + + ))} ); diff --git a/frontend/src/types/club.ts b/frontend/src/types/club.ts index b377843ea..8d17560a7 100644 --- a/frontend/src/types/club.ts +++ b/frontend/src/types/club.ts @@ -1,3 +1,5 @@ +import { SNS_CONFIG } from '@/constants/snsConfig'; + export interface Club { id: string; name: string; @@ -9,6 +11,8 @@ export interface Club { introduction: string; } +export type SNSPlatform = keyof typeof SNS_CONFIG; + export interface ClubDetail extends Club { description: string; state: string; @@ -18,6 +22,7 @@ export interface ClubDetail extends Club { recruitmentForm: string; recruitmentPeriod: string; recruitmentTarget: string; + socialLinks: Record; } export interface ClubDescription { diff --git a/frontend/src/utils/validateSocialLink.test.ts b/frontend/src/utils/validateSocialLink.test.ts new file mode 100644 index 000000000..6a79aa8de --- /dev/null +++ b/frontend/src/utils/validateSocialLink.test.ts @@ -0,0 +1,75 @@ +import { validateSocialLink } from '@/utils/validateSocialLink'; +import { SNS_CONFIG } from '@/constants/snsConfig'; +import { SNSPlatform } from '@/types/club'; + +type LinkSet = Record; + +const validLinks: LinkSet = { + instagram: 'https://www.instagram.com/valid_id', + youtube: 'https://www.youtube.com/@validchannel', + x: 'https://x.com/valid_user', +}; + +const invalidLinks: LinkSet = { + instagram: 'https://instagram.com', + youtube: 'youtube.com/123', + x: 'https://x.com/', +}; + +const phishingLinks: LinkSet = { + instagram: 'https://www.instagram.evil.com/id', + youtube: 'https://www.y0utube.com/@channel', + x: 'https://x.co/valid_user', +}; + +const platforms = Object.keys(SNS_CONFIG) as SNSPlatform[]; + +describe('validateSocialLink - SNS 링크 유효성 검사', () => { + const expectNoError = (platform: SNSPlatform, link: string) => { + expect(validateSocialLink(platform, link)).toBe(''); + }; + + const expectError = (platform: SNSPlatform, link: string) => { + expect(validateSocialLink(platform, link)).toBe( + `${SNS_CONFIG[platform].label} 링크 형식이 조금 이상해요. 다시 한 번 확인해주세요!`, + ); + }; + + it('유효한 링크는 에러 메시지를 반환하지 않는다', () => { + platforms.forEach((platform) => + expectNoError(platform, validLinks[platform]), + ); + }); + + it('잘못된 형식의 링크는 에러 메시지를 반환한다', () => { + platforms.forEach((platform) => + expectError(platform, invalidLinks[platform]), + ); + }); + + it('입력값이 비어있으면 에러 메시지를 반환하지 않는다', () => { + platforms.forEach((platform) => expectNoError(platform, '')); + }); + + it('피싱 가능성이 있는 링크는 에러 메시지를 반환한다', () => { + platforms.forEach((platform) => + expectError(platform, phishingLinks[platform]), + ); + }); + + it('http 링크는 허용되지 않아야 한다', () => { + expectError('x', 'http://x.com/valid_id'); + }); + + it('유니코드 기반 도메인 위장은 허용되지 않아야 한다', () => { + // 러시아어 소문자 о (U+043E) + const spoofedUrl = 'https://www.yоutube.com/@fake'; + expectError('youtube', spoofedUrl); + }); + + it('redirect 파라미터로 외부 사이트 유도 시 에러 메시지를 반환해야 한다', () => { + const redirectUrl = + 'https://www.instagram.com/redirect?to=https://attacker.com/steal?cookie=document.cookie'; + expectError('instagram', redirectUrl); + }); +}); diff --git a/frontend/src/utils/validateSocialLink.ts b/frontend/src/utils/validateSocialLink.ts new file mode 100644 index 000000000..750fe2823 --- /dev/null +++ b/frontend/src/utils/validateSocialLink.ts @@ -0,0 +1,10 @@ +import { SNS_CONFIG } from '@/constants/snsConfig'; +import { SNSPlatform } from '@/types/club'; + +export const validateSocialLink = (key: SNSPlatform, value: string): string => { + const { regex, label } = SNS_CONFIG[key]; + if (value && !regex.test(value)) { + return `${label} 링크 형식이 조금 이상해요. 다시 한 번 확인해주세요!`; + } + return ''; +};