diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index f1d90350..334532fa 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -9,22 +9,36 @@ module.exports = {
'prettier',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
+ 'plugin:@tanstack/eslint-plugin-query/recommended',
],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
- ecmaVersion: 12,
+ ecmaVersion: 'latest',
sourceType: 'module',
},
parser: '@typescript-eslint/parser',
- plugins: ['react', 'prettier', '@typescript-eslint'],
+ plugins: ['react', 'prettier', '@typescript-eslint', 'jsx-a11y', '@emotion'],
rules: {
- 'prettier/prettier': 'error',
+ 'prettier/prettier': ['error', { endOfLine: 'auto' }],
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'no-console': 'warn',
- 'no-unused-vars': 'warn',
- 'prettier/prettier': ['error', { endOfLine: 'auto' }],
+ '@typescript-eslint/no-unused-vars': 'error',
+ 'no-console': ['warn', { allow: ['warn', 'error'] }],
+ '@tanstack/query/exhaustive-deps': 'error',
+ '@tanstack/query/stable-query-client': 'error',
+ '@tanstack/query/no-rest-destructuring': 'warn',
+ },
+ settings: {
+ react: { version: 'detect' },
+ 'import/resolver': {
+ alias: {
+ map: [['@', './src']],
+ extensions: ['.ts', '.js', '.jsx', '.json'],
+ },
+ },
},
}
diff --git a/.firebaserc b/.firebaserc
new file mode 100644
index 00000000..3a69b645
--- /dev/null
+++ b/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "np-no-problem"
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml
index 522b620a..8c9fa646 100644
--- a/.github/ISSUE_TEMPLATE/feature.yml
+++ b/.github/ISSUE_TEMPLATE/feature.yml
@@ -18,7 +18,7 @@ body:
description: 해야 할 일에 대한 Tasks를 작성해 주세요.
placeholder: 최대한 세분화해서 작성! (체크박스 활용하기)
validations:
- required: true
+ required: false
- type: textarea
id: feature-memo
attributes:
diff --git a/.github/ISSUE_TEMPLATE/refactor.yml b/.github/ISSUE_TEMPLATE/refactor.yml
index a6fe3ece..da02a1d8 100644
--- a/.github/ISSUE_TEMPLATE/refactor.yml
+++ b/.github/ISSUE_TEMPLATE/refactor.yml
@@ -18,7 +18,7 @@ body:
description: 해야 할 일에 대한 Tasks를 작성해 주세요.
placeholder: 최대한 세분화해서 작성! (체크박스 활용하기)
validations:
- required: true
+ required: false
- type: textarea
id: refactor-memo
attributes:
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 9522b834..9f5561ee 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,27 +1,11 @@
-# 🚀 풀 리퀘스트 제안
+## 📋 풀리퀘스트 관련 코멘트
-
+
-
-
-## 📋 작업 내용
-
-수정한 내용이나 추가한 기능에 대해 자세히 설명해 주세요.
-
-## 🔧 변경 사항
-
-- [ ] 📃 README.md
-- [ ] 📦 package.json
-- [ ] 🔥 파일 삭제
-- [ ] 🧹 그 외 ex) .gitignore 등
-
-주요 변경 사항을 요약해 주세요.
+
## 📸 스크린샷 (선택 사항)
-수정된 화면 또는 기능을 시연할 수 있는 스크린샷을 첨부해 주세요.
-
-## 📄 기타
-
-추가적으로 전달하고 싶은 내용이나 특별한 요구 사항이 있으면 작성해 주세요.
+
+
diff --git a/.github/workflows/click_cd.yml b/.github/workflows/click_cd.yml
new file mode 100644
index 00000000..32abd273
--- /dev/null
+++ b/.github/workflows/click_cd.yml
@@ -0,0 +1,35 @@
+name: Firebase Hosting by Click
+
+on:
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup node.js 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20
+ cache: 'npm'
+ - name: Install
+ run: npm ci
+ - name: Build
+ env:
+ VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }}
+ VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}
+ VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }}
+ VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}
+ VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}
+ VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }}
+ VITE_FIREBASE_MEASUREMENT_ID: ${{ secrets.VITE_FIREBASE_MEASUREMENT_ID }}
+ VITE_FIREBASE_REALTIME_DB: ${{ secrets.VITE_FIREBASE_REALTIME_DB }}
+ VITE_YOUTUBE_API_KEY: ${{ secrets.VITE_YOUTUBE_API_KEY }}
+
+ run: npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
+ projectId: np-no-problem
+ channelId: live
diff --git a/.github/workflows/pr_auto_cd.yml b/.github/workflows/pr_auto_cd.yml
new file mode 100644
index 00000000..64a3d26a
--- /dev/null
+++ b/.github/workflows/pr_auto_cd.yml
@@ -0,0 +1,36 @@
+name: PR Auto Hosting
+
+on:
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup node.js 20.x
+ uses: actions/setup-node@v3
+ with:
+ node-version: 20
+ cache: 'npm'
+ - name: Install
+ run: npm ci
+ - name: Build
+ env:
+ VITE_FIREBASE_API_KEY: ${{ secrets.VITE_FIREBASE_API_KEY }}
+ VITE_FIREBASE_AUTH_DOMAIN: ${{ secrets.VITE_FIREBASE_AUTH_DOMAIN }}
+ VITE_FIREBASE_PROJECT_ID: ${{ secrets.VITE_FIREBASE_PROJECT_ID }}
+ VITE_FIREBASE_STORAGE_BUCKET: ${{ secrets.VITE_FIREBASE_STORAGE_BUCKET }}
+ VITE_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.VITE_FIREBASE_MESSAGING_SENDER_ID }}
+ VITE_FIREBASE_APP_ID: ${{ secrets.VITE_FIREBASE_APP_ID }}
+ VITE_FIREBASE_MEASUREMENT_ID: ${{ secrets.VITE_FIREBASE_MEASUREMENT_ID }}
+ VITE_FIREBASE_REALTIME_DB: ${{ secrets.VITE_FIREBASE_REALTIME_DB }}
+ VITE_YOUTUBE_API_KEY: ${{ secrets.VITE_YOUTUBE_API_KEY }}
+ run: npm run build
+ - uses: FirebaseExtended/action-hosting-deploy@v0
+ with:
+ firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
+ projectId: np-no-problem
+ channelId: live
diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml
index 16e649a3..9f386cd6 100644
--- a/.github/workflows/sonar.yml
+++ b/.github/workflows/sonar.yml
@@ -1,9 +1,6 @@
name: SonarQube Scan
-on:
- push:
- branches:
- - "*"
+on: workflow_dispatch
jobs:
AI-Server:
@@ -18,6 +15,6 @@ jobs:
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
- LC_ALL: "ko_KR.UTF-8"
- LANG: "ko_KR.UTF-8"
+ LC_ALL: 'ko_KR.UTF-8'
+ LANG: 'ko_KR.UTF-8'
# https://ai.nowon.store
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 84b747e2..00000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-name: Build Test
-
-on:
- push:
- branches: [main]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout Source Code
- uses: actions/checkout@v3
- - name: Setup node.js 20.x
- uses: actions/setup-node@v3
- with:
- node-version: 20
- cache: "npm"
- - name: Install dependencies
- run: npm install
diff --git a/.gitignore b/.gitignore
index 926b4aab..438801db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,9 @@ dist
dist-ssr
*.local
+# Firebase
+.firebase/*
+
Dockerfile
docker.js
docker.yml
@@ -26,3 +29,5 @@ docker.yml
*.njsproj
*.sln
*.sw?
+
+.env
\ No newline at end of file
diff --git a/.gitmojirc.json b/.gitmojirc.json
new file mode 100644
index 00000000..83c96eaa
--- /dev/null
+++ b/.gitmojirc.json
@@ -0,0 +1,8 @@
+{
+ "autoAdd": false,
+ "emojiFormat": "emoji",
+ "scopePrompt": false,
+ "messagePrompt": false,
+ "capitalizeTitle": false,
+ "gitmojisUrl": "https://gist.githubusercontent.com/seoyoonyi/8d8db43873ad4908a0fdb9c4f18737c4/raw/28cb248201e0001a035014c14a25f8d1636dee91/gitmoji.json"
+}
diff --git a/.husky/pre-commit b/.husky/pre-commit
index d24fdfc6..0312b760 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
-npx lint-staged
+npx lint-staged
\ No newline at end of file
diff --git a/README.md b/README.md
index e1cdc89d..07675175 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,3 @@
-# React + TypeScript + Vite
+# NP - No Problem
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
-
-- Configure the top-level `parserOptions` property like this:
-
-```js
-export default {
- // other rules...
- parserOptions: {
- ecmaVersion: 'latest',
- sourceType: 'module',
- project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: __dirname,
- },
-}
-```
-
-- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
-- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
-- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
+# My Idoru
diff --git a/database.rules.json b/database.rules.json
new file mode 100644
index 00000000..d2b8268c
--- /dev/null
+++ b/database.rules.json
@@ -0,0 +1,6 @@
+{
+ "rules": {
+ ".read": true,
+ ".write": true
+ }
+}
diff --git a/firebase.json b/firebase.json
new file mode 100644
index 00000000..b91f3dda
--- /dev/null
+++ b/firebase.json
@@ -0,0 +1,15 @@
+{
+ "hosting": {
+ "public": "dist",
+ "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
+ "rewrites": [
+ {
+ "source": "**",
+ "destination": "/index.html"
+ }
+ ]
+ },
+ "database": {
+ "rules": "database.rules.json"
+ }
+}
diff --git a/index.html b/index.html
index e4b78eae..7d959a39 100644
--- a/index.html
+++ b/index.html
@@ -1,10 +1,21 @@
-
+
-
+
+
+
+
- Vite + React + TS
+ My Idoru
diff --git a/np_logo.svg b/np_logo.svg
new file mode 100644
index 00000000..bb81139a
--- /dev/null
+++ b/np_logo.svg
@@ -0,0 +1,89 @@
+
+
diff --git a/package-lock.json b/package-lock.json
index 29b92aa0..0f0d81a9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,24 +10,35 @@
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
- "@tanstack/react-query": "^5.51.24",
+ "@tanstack/react-query": "^5.52.2",
+ "firebase": "^10.13.0",
+ "lucide-react": "^0.436.0",
+ "normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hook-form": "^7.53.0",
+ "react-router-dom": "^6.26.1",
"zustand": "^4.5.5"
},
"devDependencies": {
+ "@emotion/eslint-plugin": "^11.12.0",
+ "@tanstack/eslint-plugin-query": "^5.52.0",
+ "@tanstack/react-query-devtools": "^5.52.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-emotion": "^11.0.0",
+ "eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
- "husky": "^8.0.3",
+ "husky": "^9.1.5",
"lint-staged": "^15.2.9",
"prettier": "^3.3.3",
"typescript": "^5.2.2",
@@ -60,9 +71,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.25.2",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
- "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
+ "version": "7.25.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz",
+ "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==",
"dev": true,
"engines": {
"node": ">=6.9.0"
@@ -98,6 +109,12 @@
"url": "https://opencollective.com/babel"
}
},
+ "node_modules/@babel/core/node_modules/convert-source-map": {
+ "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
+ },
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -108,11 +125,11 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
- "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz",
+ "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==",
"dependencies": {
- "@babel/types": "^7.25.0",
+ "@babel/types": "^7.25.6",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
@@ -224,13 +241,13 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
- "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.6.tgz",
+ "integrity": "sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==",
"dev": true,
"dependencies": {
"@babel/template": "^7.25.0",
- "@babel/types": "^7.25.0"
+ "@babel/types": "^7.25.6"
},
"engines": {
"node": ">=6.9.0"
@@ -251,11 +268,11 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.25.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
- "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.6.tgz",
+ "integrity": "sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==",
"dependencies": {
- "@babel/types": "^7.25.2"
+ "@babel/types": "^7.25.6"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -295,9 +312,9 @@
}
},
"node_modules/@babel/runtime": {
- "version": "7.25.0",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
- "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
+ "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -319,15 +336,15 @@
}
},
"node_modules/@babel/traverse": {
- "version": "7.25.3",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
- "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz",
+ "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==",
"dependencies": {
"@babel/code-frame": "^7.24.7",
- "@babel/generator": "^7.25.0",
- "@babel/parser": "^7.25.3",
+ "@babel/generator": "^7.25.6",
+ "@babel/parser": "^7.25.6",
"@babel/template": "^7.25.0",
- "@babel/types": "^7.25.2",
+ "@babel/types": "^7.25.6",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
@@ -336,9 +353,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.25.2",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
- "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
+ "version": "7.25.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.6.tgz",
+ "integrity": "sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==",
"dependencies": {
"@babel/helper-string-parser": "^7.24.8",
"@babel/helper-validator-identifier": "^7.24.7",
@@ -366,22 +383,6 @@
"stylis": "4.2.0"
}
},
- "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
- "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
- },
- "node_modules/@emotion/babel-plugin/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==",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/@emotion/cache": {
"version": "11.13.1",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.1.tgz",
@@ -394,6 +395,21 @@
"stylis": "4.2.0"
}
},
+ "node_modules/@emotion/eslint-plugin": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/@emotion/eslint-plugin/-/eslint-plugin-11.12.0.tgz",
+ "integrity": "sha512-N0rtAVKk6w8RchWtexdG/GFbg48tdlO4cnq9Jg6H3ul3EDDgkYkPE0PKMb1/CJ7cDyYsiNPYVc3ZnWnd2/d0tA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/utils": "^5.25.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "eslint": "6 || 7 || 8"
+ }
+ },
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
@@ -413,14 +429,14 @@
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
},
"node_modules/@emotion/react": {
- "version": "11.13.0",
- "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.0.tgz",
- "integrity": "sha512-WkL+bw1REC2VNV1goQyfxjx1GYJkcc23CRQkXX+vZNLINyfI7o+uUn/rTGPt/xJ3bJHd5GcljgnxHf4wRw5VWQ==",
+ "version": "11.13.3",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.3.tgz",
+ "integrity": "sha512-lIsdU6JNrmYfJ5EbUCf4xW1ovy5wKQ2CkPRM4xogziOxH1nXxBSjpC9YqbFAP7circxMfYp+6x676BqWcEiixg==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/cache": "^11.13.0",
- "@emotion/serialize": "^1.3.0",
+ "@emotion/serialize": "^1.3.1",
"@emotion/use-insertion-effect-with-fallbacks": "^1.1.0",
"@emotion/utils": "^1.4.0",
"@emotion/weak-memoize": "^0.4.0",
@@ -436,13 +452,13 @@
}
},
"node_modules/@emotion/serialize": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.0.tgz",
- "integrity": "sha512-jACuBa9SlYajnpIVXB+XOXnfJHyckDfe6fOpORIM6yhBDlqGuExvDdZYHDQGoDf3bZXGv7tNr+LpLjJqiEQ6EA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.1.tgz",
+ "integrity": "sha512-dEPNKzBPU+vFPGa+z3axPRn8XVDetYORmDC0wAiej+TNcOZE70ZMJa0X7JdeoM6q/nWTMZeLpN/fTnD9o8MQBA==",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
- "@emotion/unitless": "^0.9.0",
+ "@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.0",
"csstype": "^3.0.2"
}
@@ -475,9 +491,9 @@
}
},
"node_modules/@emotion/unitless": {
- "version": "0.9.0",
- "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.9.0.tgz",
- "integrity": "sha512-TP6GgNZtmtFaFcsOgExdnfxLLpRDla4Q66tnenA9CktvVSdNKDvMVuUah4QvWPIpNjrWsGg3qeGo9a43QooGZQ=="
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
+ "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
},
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.1.0",
@@ -958,6 +974,539 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@firebase/analytics": {
+ "version": "0.10.7",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.7.tgz",
+ "integrity": "sha512-GE29uTT6y/Jv2EP0OjpTezeTQZ5FTCTaZXKrrdVGjb/t35AU4u/jiU+hUwUPpuK8fqhhiHkS/AawE3a3ZK/a9Q==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/installations": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-compat": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.13.tgz",
+ "integrity": "sha512-aZ4wGfNDMsCxhKzDbK2g1aV0JKsdQ9FbeIsjpNJPzhahV0XYj+z36Y4RNLPpG/6hHU4gxnezxs+yn3HhHkNL8w==",
+ "dependencies": {
+ "@firebase/analytics": "0.10.7",
+ "@firebase/analytics-types": "0.8.2",
+ "@firebase/component": "0.6.8",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/analytics-types": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.2.tgz",
+ "integrity": "sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw=="
+ },
+ "node_modules/@firebase/app": {
+ "version": "0.10.10",
+ "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.10.tgz",
+ "integrity": "sha512-sDqkdeFdVn5uygQm5EuIKOQ6/wxTcX/qKfm0MR46AiwLRHGLCDUMrXBkc8GhkK3ca2d6mPUSfPmndggo43D6PQ==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/app-check": {
+ "version": "0.8.7",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.7.tgz",
+ "integrity": "sha512-EkOeJcMKVR0zZ6z/jqcFTqHb/xq+TVIRIuBNGHdpcIuFU1czhSlegvqv2+nC+nFrkD8M6Xvd3tAlUOkdbMeS6A==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-compat": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.14.tgz",
+ "integrity": "sha512-kK3bPfojAfXE53W+20rxMqIxrloFswXG9vh4kEdYL6Wa2IB3sD5++2dPiK3yGxl8oQiqS8qL2wcKB5/xLpEVEg==",
+ "dependencies": {
+ "@firebase/app-check": "0.8.7",
+ "@firebase/app-check-types": "0.5.2",
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/app-check-interop-types": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz",
+ "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ=="
+ },
+ "node_modules/@firebase/app-check-types": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.2.tgz",
+ "integrity": "sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA=="
+ },
+ "node_modules/@firebase/app-compat": {
+ "version": "0.2.40",
+ "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.40.tgz",
+ "integrity": "sha512-2L5MW4MH2ya7Wvw0hzWy3ZWeB4SqC5gYXDAV5AS1lBTL4zL3k8dsqJmry/cFV00GgkCI01WJbcXvFMCXJvgyow==",
+ "dependencies": {
+ "@firebase/app": "0.10.10",
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/app-types": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz",
+ "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ=="
+ },
+ "node_modules/@firebase/auth": {
+ "version": "1.7.8",
+ "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.8.tgz",
+ "integrity": "sha512-1KJlDrTrEEFTIBj9MxjAWjQ4skecBD4bmoayQ0l14QDbNc1a8qGbi+MFSJkH7O6VnGE6bTMcWSw6RrQNecqKaw==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0",
+ "undici": "6.19.7"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@react-native-async-storage/async-storage": "^1.18.1"
+ },
+ "peerDependenciesMeta": {
+ "@react-native-async-storage/async-storage": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@firebase/auth-compat": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.13.tgz",
+ "integrity": "sha512-rV6TMxUU6wBBZ2ouDMtjJsJXeewtvYvVzslzt3/P7O/kxiWlreHT/2M/1guMiXKo3zk52XK3GqP0uM2bN7fEow==",
+ "dependencies": {
+ "@firebase/auth": "1.7.8",
+ "@firebase/auth-types": "0.12.2",
+ "@firebase/component": "0.6.8",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0",
+ "undici": "6.19.7"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/auth-interop-types": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz",
+ "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ=="
+ },
+ "node_modules/@firebase/auth-types": {
+ "version": "0.12.2",
+ "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.2.tgz",
+ "integrity": "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/component": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz",
+ "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==",
+ "dependencies": {
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz",
+ "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.2",
+ "@firebase/auth-interop-types": "0.2.3",
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "faye-websocket": "0.11.4",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database-compat": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz",
+ "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/database": "1.0.7",
+ "@firebase/database-types": "1.0.4",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/database-types": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz",
+ "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==",
+ "dependencies": {
+ "@firebase/app-types": "0.9.2",
+ "@firebase/util": "1.9.7"
+ }
+ },
+ "node_modules/@firebase/firestore": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.1.tgz",
+ "integrity": "sha512-WliQNa8GVcH6EWkH0NAf+uAnxNiBuH+G8Buzr2ZS1NznOhJDK/q6Hsjv5TzNrijLTAdEfj/wk9VEv994KDSjxg==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "@firebase/webchannel-wrapper": "1.0.1",
+ "@grpc/grpc-js": "~1.9.0",
+ "@grpc/proto-loader": "^0.7.8",
+ "tslib": "^2.1.0",
+ "undici": "6.19.7"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-compat": {
+ "version": "0.3.36",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.36.tgz",
+ "integrity": "sha512-NtoIm7CT9f+SFB7cPMCtyCSxZReh/+SII5X4TQH394S3dwhru9HIfvEOKAMuAnXsSsLH72jXPUgdsEAUqg6Oug==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/firestore": "4.7.1",
+ "@firebase/firestore-types": "3.0.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/firestore-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.2.tgz",
+ "integrity": "sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/functions": {
+ "version": "0.11.7",
+ "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.7.tgz",
+ "integrity": "sha512-xaUsGI2kYrI8zJXgrNB7SrJKB8v1vJqR16YYi6g6dFTgBz4+VzWJFqqVU60BbdAWm6fXnUrg9gjlJQeqomT2Vg==",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.2",
+ "@firebase/auth-interop-types": "0.2.3",
+ "@firebase/component": "0.6.8",
+ "@firebase/messaging-interop-types": "0.2.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0",
+ "undici": "6.19.7"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-compat": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.13.tgz",
+ "integrity": "sha512-qcZvJO2ed6PAD+18DanVztw7WyQVQK43HoRhxusHAwDFvK/xY+mcGpj+IpfdxTNMBGCOIxKFp4Xqk/c2nubBlQ==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/functions": "0.11.7",
+ "@firebase/functions-types": "0.6.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/functions-types": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.2.tgz",
+ "integrity": "sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w=="
+ },
+ "node_modules/@firebase/installations": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.8.tgz",
+ "integrity": "sha512-57V374qdb2+wT5v7+ntpLXBjZkO6WRgmAUbVkRfFTM/4t980p0FesbqTAcOIiM8U866UeuuuF8lYH70D3jM/jQ==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/util": "1.9.7",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-compat": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.8.tgz",
+ "integrity": "sha512-pI2q8JFHB7yIq/szmhzGSWXtOvtzl6tCUmyykv5C8vvfOVJUH6mP4M4iwjbK8S1JotKd/K70+JWyYlxgQ0Kpyw==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/installations": "0.6.8",
+ "@firebase/installations-types": "0.5.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/installations-types": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.2.tgz",
+ "integrity": "sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/logger": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz",
+ "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/messaging": {
+ "version": "0.12.10",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.10.tgz",
+ "integrity": "sha512-fGbxJPKpl2DIKNJGhbk4mYPcM+qE2gl91r6xPoiol/mN88F5Ym6UeRdMVZah+pijh9WxM55alTYwXuW40r1Y2Q==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/installations": "0.6.8",
+ "@firebase/messaging-interop-types": "0.2.2",
+ "@firebase/util": "1.9.7",
+ "idb": "7.1.1",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-compat": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.10.tgz",
+ "integrity": "sha512-FXQm7rcowkDm8kFLduHV35IRYCRo+Ng0PIp/t1+EBuEbyplaKkGjZ932pE+owf/XR+G/60ku2QRBptRGLXZydg==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/messaging": "0.12.10",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/messaging-interop-types": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz",
+ "integrity": "sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA=="
+ },
+ "node_modules/@firebase/performance": {
+ "version": "0.6.8",
+ "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.8.tgz",
+ "integrity": "sha512-F+alziiIZ6Yn8FG47mxwljq+4XkgkT2uJIFRlkyViUQRLzrogaUJW6u/+6ZrePXnouKlKIwzqos3PVJraPEcCA==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/installations": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-compat": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.8.tgz",
+ "integrity": "sha512-o7TFClRVJd3VIBoY7KZQqtCeW0PC6v9uBzM6Lfw3Nc9D7hM6OonqecYvh7NwJ6R14k+xM27frLS4BcCvFHKw2A==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/performance": "0.6.8",
+ "@firebase/performance-types": "0.2.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/performance-types": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.2.tgz",
+ "integrity": "sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA=="
+ },
+ "node_modules/@firebase/remote-config": {
+ "version": "0.4.8",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.8.tgz",
+ "integrity": "sha512-AMLqe6wfIRnjc6FkCWOSUjhc1fSTEf8o+cv1NolFvbiJ/tU+TqN4pI7pT+MIKQzNiq5fxLehkOx+xtAQBxPJKQ==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/installations": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-compat": {
+ "version": "0.2.8",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.8.tgz",
+ "integrity": "sha512-UxSFOp6dzFj2AHB8Bq/BYtbq5iFyizKx4Rd6WxAdaKYM8cnPMeK+l2v+Oogtjae+AeyHRI+MfL2acsfVe5cd2A==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/remote-config": "0.4.8",
+ "@firebase/remote-config-types": "0.3.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/remote-config-types": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz",
+ "integrity": "sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA=="
+ },
+ "node_modules/@firebase/storage": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.1.tgz",
+ "integrity": "sha512-L6AJ5tWgHSi2g/gbc/2Pbm3qxmoEg9THmPIOpRsLwuz9LPeWbhyMQeGlqxWqtZGQO/z/LMjGYadNlupQj0HNfw==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0",
+ "undici": "6.19.7"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-compat": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.11.tgz",
+ "integrity": "sha512-EEa9jgm/aRVIGSD0ByYAsZ0tvEKfVwSp9uFoa/97BISGWGjSNPIWjenaDvpDZ7aL8OxaGIpwuk700aHy7/T0Ug==",
+ "dependencies": {
+ "@firebase/component": "0.6.8",
+ "@firebase/storage": "0.13.1",
+ "@firebase/storage-types": "0.8.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "@firebase/app-compat": "0.x"
+ }
+ },
+ "node_modules/@firebase/storage-types": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.2.tgz",
+ "integrity": "sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==",
+ "peerDependencies": {
+ "@firebase/app-types": "0.x",
+ "@firebase/util": "1.x"
+ }
+ },
+ "node_modules/@firebase/util": {
+ "version": "1.9.7",
+ "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz",
+ "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@firebase/vertexai-preview": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.3.tgz",
+ "integrity": "sha512-KVtUWLp+ScgiwkDKAvNkVucAyhLVQp6C6lhnVEuIg4mWhWcS3oerjAeVhZT4uNofKwWxRsOaB2Yec7DMTXlQPQ==",
+ "dependencies": {
+ "@firebase/app-check-interop-types": "0.3.2",
+ "@firebase/component": "0.6.8",
+ "@firebase/logger": "0.4.2",
+ "@firebase/util": "1.9.7",
+ "tslib": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "@firebase/app": "0.x",
+ "@firebase/app-types": "0.x"
+ }
+ },
+ "node_modules/@firebase/webchannel-wrapper": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz",
+ "integrity": "sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ=="
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.9.15",
+ "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz",
+ "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.7.8",
+ "@types/node": ">=12.12.47"
+ },
+ "engines": {
+ "node": "^8.13.0 || >=10.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.7.13",
+ "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz",
+ "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==",
+ "dependencies": {
+ "lodash.camelcase": "^4.3.0",
+ "long": "^5.0.0",
+ "protobufjs": "^7.2.5",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1105,10 +1654,72 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
+ "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
+ "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
+ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
+ "integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
- "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
+ "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [
"arm"
],
@@ -1119,9 +1730,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
- "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
+ "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [
"arm64"
],
@@ -1132,9 +1743,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
- "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
+ "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [
"arm64"
],
@@ -1145,9 +1756,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
- "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
+ "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [
"x64"
],
@@ -1158,9 +1769,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
- "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
+ "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
"cpu": [
"arm"
],
@@ -1171,9 +1782,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
- "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
+ "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
"cpu": [
"arm"
],
@@ -1184,9 +1795,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
- "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
+ "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [
"arm64"
],
@@ -1197,9 +1808,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
- "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
+ "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [
"arm64"
],
@@ -1210,9 +1821,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
- "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
+ "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [
"ppc64"
],
@@ -1223,9 +1834,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
- "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
+ "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [
"riscv64"
],
@@ -1236,9 +1847,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
- "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
+ "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [
"s390x"
],
@@ -1249,9 +1860,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
- "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
+ "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [
"x64"
],
@@ -1262,9 +1873,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
- "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
+ "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [
"x64"
],
@@ -1275,9 +1886,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
- "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
+ "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [
"arm64"
],
@@ -1288,9 +1899,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
- "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
+ "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [
"ia32"
],
@@ -1301,9 +1912,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
- "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
+ "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [
"x64"
],
@@ -1313,28 +1924,168 @@
"win32"
]
},
- "node_modules/@tanstack/query-core": {
- "version": "5.51.24",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.24.tgz",
- "integrity": "sha512-qtIR0FMHUDIWyIQw87q4C+so7XaN59MsGfWrc6rgi2VTHrVZF3Hd0St2dbpqRetHf6XW5yY5lzTrXpTilPlxUg==",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/tannerlinsley"
- }
- },
- "node_modules/@tanstack/react-query": {
- "version": "5.51.24",
- "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.24.tgz",
- "integrity": "sha512-sW1qRwoCDqOFku67xng4Y5z6NPK1DS347jR4RiX9wFHrmyqpbXgUjPIjT3fodezdJAaSJD/6CvWb0cl05J8zNQ==",
+ "node_modules/@tanstack/eslint-plugin-query": {
+ "version": "5.56.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.56.1.tgz",
+ "integrity": "sha512-IUm2Zy5BXOqMbaa7QwNg3cPa5NP5Rm3pIFCFpe7Y3pLC7Ftp8Q0Y8GU2uNpCbMFW79jHJXdQ4Oxnu1eTQr8GXQ==",
+ "dev": true,
"dependencies": {
- "@tanstack/query-core": "5.51.24"
+ "@typescript-eslint/utils": "^8.3.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
- "react": "^18.0.0"
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
+ "integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.5.0",
+ "@typescript-eslint/visitor-keys": "8.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
+ "integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
+ "integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.5.0",
+ "@typescript-eslint/visitor-keys": "8.5.0",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "is-glob": "^4.0.3",
+ "minimatch": "^9.0.4",
+ "semver": "^7.6.0",
+ "ts-api-utils": "^1.3.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
+ "integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "8.5.0",
+ "@typescript-eslint/types": "8.5.0",
+ "@typescript-eslint/typescript-estree": "8.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0"
+ }
+ },
+ "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
+ "integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "8.5.0",
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.56.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.56.2.tgz",
+ "integrity": "sha512-gor0RI3/R5rVV3gXfddh1MM+hgl0Z4G7tj6Xxpq6p2I03NGPaJ8dITY9Gz05zYYb/EJq9vPas/T4wn9EaDPd4Q==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.56.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.56.1.tgz",
+ "integrity": "sha512-xnp9jq/9dHfSCDmmf+A5DjbIjYqbnnUL2ToqlaaviUQGRTapXQ8J+GxusYUu1IG0vZMaWdiVUA4HRGGZYAUU+A==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.56.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.56.2.tgz",
+ "integrity": "sha512-SR0GzHVo6yzhN72pnRhkEFRAHMsUo5ZPzAxfTMvUxFIDVS6W9LYUp6nXW3fcHVdg0ZJl8opSH85jqahvm6DSVg==",
+ "dependencies": {
+ "@tanstack/query-core": "5.56.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.56.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.56.2.tgz",
+ "integrity": "sha512-7nINJtRZZVwhTTyDdMIcSaXo+EHMLYJu1S2e6FskvvD5prx87LlAXXWZDfU24Qm4HjshEtM5lS3HIOszNGblcw==",
+ "dev": true,
+ "dependencies": {
+ "@tanstack/query-devtools": "5.56.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.56.2",
+ "react": "^18 || ^19"
}
},
"node_modules/@types/babel__core": {
@@ -1384,6 +2135,20 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
},
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/node": {
+ "version": "22.5.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz",
+ "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -1396,9 +2161,9 @@
"devOptional": true
},
"node_modules/@types/react": {
- "version": "18.3.3",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
- "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
+ "version": "18.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz",
+ "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==",
"devOptional": true,
"dependencies": {
"@types/prop-types": "*",
@@ -1414,6 +2179,18 @@
"@types/react": "*"
}
},
+ "node_modules/@types/semver": {
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+ "dev": true
+ },
+ "node_modules/@types/uuid": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
+ "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
+ "dev": true
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -1447,6 +2224,28 @@
}
}
},
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
"node_modules/@typescript-eslint/parser": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
@@ -1519,6 +2318,28 @@
}
}
},
+ "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
+ "version": "7.18.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+ "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "@typescript-eslint/scope-manager": "7.18.0",
+ "@typescript-eslint/types": "7.18.0",
+ "@typescript-eslint/typescript-estree": "7.18.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || >=20.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.56.0"
+ }
+ },
"node_modules/@typescript-eslint/types": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
@@ -1561,25 +2382,103 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.18.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
- "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz",
+ "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==",
"dev": true,
"dependencies": {
- "@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "7.18.0",
- "@typescript-eslint/types": "7.18.0",
- "@typescript-eslint/typescript-estree": "7.18.0"
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@types/json-schema": "^7.0.9",
+ "@types/semver": "^7.3.12",
+ "@typescript-eslint/scope-manager": "5.62.0",
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/typescript-estree": "5.62.0",
+ "eslint-scope": "^5.1.1",
+ "semver": "^7.3.7"
},
"engines": {
- "node": "^18.18.0 || >=20.0.0"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "eslint": "^8.56.0"
+ "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/scope-manager": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz",
+ "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/types": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz",
+ "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz",
+ "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "@typescript-eslint/visitor-keys": "5.62.0",
+ "debug": "^4.3.4",
+ "globby": "^11.1.0",
+ "is-glob": "^4.0.3",
+ "semver": "^7.3.7",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/visitor-keys": {
+ "version": "5.62.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz",
+ "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==",
+ "dev": true,
+ "dependencies": {
+ "@typescript-eslint/types": "5.62.0",
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
@@ -1680,7 +2579,6 @@
"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"
}
@@ -1702,6 +2600,15 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
+ "node_modules/aria-query": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
+ "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
+ "dev": true,
+ "dependencies": {
+ "deep-equal": "^2.0.5"
+ }
+ },
"node_modules/array-buffer-byte-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
@@ -1841,6 +2748,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -1856,6 +2769,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/axe-core": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.0.tgz",
+ "integrity": "sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
@@ -1957,9 +2888,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001651",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
- "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+ "version": "1.0.30001660",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
+ "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
"dev": true,
"funding": [
{
@@ -1989,6 +2920,14 @@
"node": ">=4"
}
},
+ "node_modules/chalk/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==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -2020,6 +2959,91 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/cliui/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -2055,10 +3079,9 @@
"dev": true
},
"node_modules/convert-source-map": {
- "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
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
@@ -2094,6 +3117,12 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
+ "node_modules/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
"node_modules/data-view-buffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz",
@@ -2146,11 +3175,11 @@
}
},
"node_modules/debug": {
- "version": "4.3.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
- "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -2161,6 +3190,38 @@
}
}
},
+ "node_modules/deep-equal": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
+ "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.0",
+ "call-bind": "^1.0.5",
+ "es-get-iterator": "^1.1.3",
+ "get-intrinsic": "^1.2.2",
+ "is-arguments": "^1.1.1",
+ "is-array-buffer": "^3.0.2",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.2",
+ "isarray": "^2.0.5",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.4",
+ "regexp.prototype.flags": "^1.5.1",
+ "side-channel": "^1.0.4",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2226,15 +3287,15 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.12",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.12.tgz",
- "integrity": "sha512-tIhPkdlEoCL1Y+PToq3zRNehUaKp3wBX/sr7aclAWdIWjvqAe/Im/H0SiCM4c1Q8BLPHCdoJTol+ZblflydehA==",
+ "version": "1.5.21",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.21.tgz",
+ "integrity": "sha512-+rBAerCpQvFSPyAO677i5gJuWGO2WFsoujENdcMzsrpP7Ebcc3pmpERgU8CV4fFF10a5haP4ivnFQ/AmLICBVg==",
"dev": true
},
"node_modules/emoji-regex": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
- "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true
},
"node_modules/environment": {
@@ -2338,6 +3399,26 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-get-iterator": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
+ "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.3",
+ "has-symbols": "^1.0.3",
+ "is-arguments": "^1.1.1",
+ "is-map": "^2.0.2",
+ "is-set": "^2.0.2",
+ "is-string": "^1.0.7",
+ "isarray": "^2.0.5",
+ "stop-iteration-iterator": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/es-iterator-helpers": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz",
@@ -2454,20 +3535,22 @@
}
},
"node_modules/escalade": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
- "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
- "dev": true,
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"engines": {
"node": ">=6"
}
},
"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==",
"engines": {
- "node": ">=0.8.0"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
@@ -2510,31 +3593,92 @@
"levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3",
- "strip-ansi": "^6.0.1",
- "text-table": "^0.2.0"
- },
- "bin": {
- "eslint": "bin/eslint.js"
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
+ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "dev": true,
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-emotion": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-emotion/-/eslint-plugin-emotion-11.0.0.tgz",
+ "integrity": "sha512-SKYyFYRFgOQTtBetn1mTKC2V1J6sbTnJh2/giWzg/OaTBSvJFwew8OkoxA3R9IhyaXYyPE+1TWzRhUJvVvBjVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.10.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz",
+ "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==",
+ "dev": true,
+ "dependencies": {
+ "aria-query": "~5.1.3",
+ "array-includes": "^3.1.8",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "^4.10.0",
+ "axobject-query": "^4.1.0",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "es-iterator-helpers": "^1.0.19",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.includes": "^2.0.0"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ "node": ">=4.0"
},
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
}
},
- "node_modules/eslint-config-prettier": {
- "version": "9.1.0",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
- "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
- "bin": {
- "eslint-config-prettier": "bin/cli.js"
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
},
- "peerDependencies": {
- "eslint": ">=7.0.0"
+ "engines": {
+ "node": "*"
}
},
"node_modules/eslint-plugin-prettier": {
@@ -2568,9 +3712,9 @@
}
},
"node_modules/eslint-plugin-react": {
- "version": "7.35.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz",
- "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==",
+ "version": "7.36.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.36.1.tgz",
+ "integrity": "sha512-/qwbqNXZoq+VP30s1d4Nc1C5GTxjJQjk4Jzs4Wq2qzxFM7dSmuG2UkIjg2USMLh3A/aVcUNrK7v0J5U1XEGGwA==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.8",
@@ -2612,9 +3756,9 @@
}
},
"node_modules/eslint-plugin-react-refresh": {
- "version": "0.4.9",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz",
- "integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==",
+ "version": "0.4.11",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.11.tgz",
+ "integrity": "sha512-wrAKxMbVr8qhXTtIKfXqAn5SAtRZt0aXxe5P23Fh4pUAdC6XEsybGLB8P0PI4j1yYqOgUEUlzKAGDfo7rJOjcw==",
"dev": true,
"peerDependencies": {
"eslint": ">=7"
@@ -2681,19 +3825,25 @@
}
},
"node_modules/eslint-scope": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
- "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
+ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"dev": true,
"dependencies": {
"esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
+ "estraverse": "^4.1.1"
},
"engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-scope/node_modules/estraverse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
+ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
}
},
"node_modules/eslint-visitor-keys": {
@@ -2767,16 +3917,20 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
- "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==",
+ "node_modules/eslint/node_modules/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
"dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
"engines": {
- "node": ">=10"
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/globals": {
@@ -2976,6 +4130,17 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/faye-websocket": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz",
+ "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==",
+ "dependencies": {
+ "websocket-driver": ">=0.5.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -3021,6 +4186,40 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/firebase": {
+ "version": "10.13.1",
+ "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.13.1.tgz",
+ "integrity": "sha512-L5BSkmvB2dzCUMpr8i/O8WMJC3Nqj5Ld8Wj/qnak+tz2Ga+JH6/FO93xArg9IGhktCrPXVODoWp6t9ybdgmXCA==",
+ "dependencies": {
+ "@firebase/analytics": "0.10.7",
+ "@firebase/analytics-compat": "0.2.13",
+ "@firebase/app": "0.10.10",
+ "@firebase/app-check": "0.8.7",
+ "@firebase/app-check-compat": "0.3.14",
+ "@firebase/app-compat": "0.2.40",
+ "@firebase/app-types": "0.9.2",
+ "@firebase/auth": "1.7.8",
+ "@firebase/auth-compat": "0.5.13",
+ "@firebase/database": "1.0.7",
+ "@firebase/database-compat": "1.0.7",
+ "@firebase/firestore": "4.7.1",
+ "@firebase/firestore-compat": "0.3.36",
+ "@firebase/functions": "0.11.7",
+ "@firebase/functions-compat": "0.3.13",
+ "@firebase/installations": "0.6.8",
+ "@firebase/installations-compat": "0.2.8",
+ "@firebase/messaging": "0.12.10",
+ "@firebase/messaging-compat": "0.2.10",
+ "@firebase/performance": "0.6.8",
+ "@firebase/performance-compat": "0.2.8",
+ "@firebase/remote-config": "0.4.8",
+ "@firebase/remote-config-compat": "0.2.8",
+ "@firebase/storage": "0.13.1",
+ "@firebase/storage-compat": "0.3.11",
+ "@firebase/util": "1.9.7",
+ "@firebase/vertexai-preview": "0.0.3"
+ }
+ },
"node_modules/flat-cache": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
@@ -3114,6 +4313,14 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-east-asian-width": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
@@ -3378,6 +4585,11 @@
"react-is": "^16.7.0"
}
},
+ "node_modules/http-parser-js": {
+ "version": "0.5.8",
+ "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz",
+ "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q=="
+ },
"node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
@@ -3388,20 +4600,25 @@
}
},
"node_modules/husky": {
- "version": "8.0.3",
- "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz",
- "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==",
+ "version": "9.1.6",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz",
+ "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==",
"dev": true,
"bin": {
- "husky": "lib/bin.js"
+ "husky": "bin.js"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
+ "node_modules/idb": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3466,6 +4683,22 @@
"node": ">= 0.4"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@@ -3543,9 +4776,9 @@
}
},
"node_modules/is-core-module": {
- "version": "2.15.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
- "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
+ "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dependencies": {
"hasown": "^2.0.2"
},
@@ -3955,6 +5188,24 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.23",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
+ "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -3986,9 +5237,9 @@
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/lint-staged": {
- "version": "15.2.9",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.9.tgz",
- "integrity": "sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==",
+ "version": "15.2.10",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz",
+ "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==",
"dev": true,
"dependencies": {
"chalk": "~5.3.0",
@@ -3997,7 +5248,7 @@
"execa": "~8.0.1",
"lilconfig": "~3.1.2",
"listr2": "~8.2.4",
- "micromatch": "~4.0.7",
+ "micromatch": "~4.0.8",
"pidtree": "~0.6.0",
"string-argv": "~0.3.2",
"yaml": "~2.5.0"
@@ -4025,9 +5276,9 @@
}
},
"node_modules/lint-staged/node_modules/yaml": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
- "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
+ "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"dev": true,
"bin": {
"yaml": "bin.mjs"
@@ -4068,6 +5319,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
+ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -4094,9 +5350,9 @@
}
},
"node_modules/log-update/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "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"
@@ -4163,6 +5419,11 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/long": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
+ "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
+ },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -4183,6 +5444,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.436.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.436.0.tgz",
+ "integrity": "sha512-N292bIxoqm1aObAg0MzFtvhYwgQE6qnIOWx/GLj5ONgcTPH6N0fD9bVq/GfdeC9ZORBXozt/XeEKDpiB3x3vlQ==",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -4199,9 +5468,9 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
- "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"dependencies": {
"braces": "^3.0.3",
@@ -4251,9 +5520,9 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/nanoid": {
"version": "3.3.7",
@@ -4285,6 +5554,11 @@
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"dev": true
},
+ "node_modules/normalize.css": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz",
+ "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg=="
+ },
"node_modules/npm-run-path": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
@@ -4333,6 +5607,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@@ -4549,9 +5839,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
- "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -4587,9 +5877,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.41",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
- "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
+ "version": "8.4.45",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
+ "integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
"dev": true,
"funding": [
{
@@ -4661,6 +5951,29 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/protobufjs": {
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
+ "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -4713,6 +6026,21 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.53.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.53.0.tgz",
+ "integrity": "sha512-M1n3HhqCww6S2hxLxciEXy2oISPnAzxY7gvwVPrtlczTM/1dDadXgUxDpHMrMTblDOcm/AXtXxHwZ3jpg1mqKQ==",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -4727,6 +6055,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
+ "integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.26.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
+ "integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
+ "dependencies": {
+ "@remix-run/router": "1.19.2",
+ "react-router": "6.26.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@@ -4771,6 +6129,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -4859,9 +6225,9 @@
}
},
"node_modules/rollup": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
- "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
+ "version": "4.21.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
+ "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
"dev": true,
"dependencies": {
"@types/estree": "1.0.5"
@@ -4874,22 +6240,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.21.0",
- "@rollup/rollup-android-arm64": "4.21.0",
- "@rollup/rollup-darwin-arm64": "4.21.0",
- "@rollup/rollup-darwin-x64": "4.21.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.21.0",
- "@rollup/rollup-linux-arm64-gnu": "4.21.0",
- "@rollup/rollup-linux-arm64-musl": "4.21.0",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.21.0",
- "@rollup/rollup-linux-s390x-gnu": "4.21.0",
- "@rollup/rollup-linux-x64-gnu": "4.21.0",
- "@rollup/rollup-linux-x64-musl": "4.21.0",
- "@rollup/rollup-win32-arm64-msvc": "4.21.0",
- "@rollup/rollup-win32-ia32-msvc": "4.21.0",
- "@rollup/rollup-win32-x64-msvc": "4.21.0",
+ "@rollup/rollup-android-arm-eabi": "4.21.3",
+ "@rollup/rollup-android-arm64": "4.21.3",
+ "@rollup/rollup-darwin-arm64": "4.21.3",
+ "@rollup/rollup-darwin-x64": "4.21.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.21.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.21.3",
+ "@rollup/rollup-linux-arm64-musl": "4.21.3",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.21.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.21.3",
+ "@rollup/rollup-linux-x64-gnu": "4.21.3",
+ "@rollup/rollup-linux-x64-musl": "4.21.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.21.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.21.3",
+ "@rollup/rollup-win32-x64-msvc": "4.21.3",
"fsevents": "~2.3.2"
}
},
@@ -4934,6 +6300,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
"node_modules/safe-regex-test": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
@@ -5100,14 +6485,26 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
+ "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
+ "dev": true,
+ "dependencies": {
+ "internal-slot": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/string-argv": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
@@ -5135,9 +6532,9 @@
}
},
"node_modules/string-width/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "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"
@@ -5146,6 +6543,12 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
+ "node_modules/string-width/node_modules/emoji-regex": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
+ "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
+ "dev": true
+ },
"node_modules/string-width/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
@@ -5161,6 +6564,16 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/string.prototype.includes": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz",
+ "integrity": "sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
@@ -5250,7 +6663,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -5364,9 +6776,29 @@
}
},
"node_modules/tslib": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
- "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
+ "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz",
+ "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==",
+ "dev": true,
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"dev": true
},
"node_modules/type-check": {
@@ -5467,9 +6899,9 @@
}
},
"node_modules/typescript": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
- "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@@ -5494,6 +6926,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/undici": {
+ "version": "6.19.7",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.7.tgz",
+ "integrity": "sha512-HR3W/bMGPSr90i8AAp2C4DM3wChFdJPLrWYpIS++LxS8K+W535qftjt+4MyjNYHeWabMj1nvtmLIi7l++iq91A==",
+ "engines": {
+ "node": ">=18.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -5542,14 +6987,14 @@
}
},
"node_modules/vite": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.1.tgz",
- "integrity": "sha512-1oE6yuNXssjrZdblI9AfBbHCC41nnyoVoEZxQnID6yvQZAFBzxxkqoFLtHUMkYunL8hwOLEjgTuxpkRxvba3kA==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.4.tgz",
+ "integrity": "sha512-RHFCkULitycHVTtelJ6jQLd+KSAAzOgEYorV32R2q++M6COBjKJR6BxqClwp5sf0XaBDjVMuJ9wnNfyAJwjMkA==",
"dev": true,
"dependencies": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.41",
- "rollup": "^4.13.0"
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -5600,6 +7045,27 @@
}
}
},
+ "node_modules/websocket-driver": {
+ "version": "0.7.4",
+ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
+ "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+ "dependencies": {
+ "http-parser-js": ">=0.5.1",
+ "safe-buffer": ">=5.1.0",
+ "websocket-extensions": ">=0.1.1"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/websocket-extensions": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz",
+ "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5721,9 +7187,9 @@
}
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
- "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "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"
@@ -5765,6 +7231,14 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -5779,6 +7253,57 @@
"node": ">= 6"
}
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/package.json b/package.json
index 7cf99cb5..e14764ed 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
- "preview": "vite preview"
+ "preview": "vite preview",
+ "prepare": "husky install"
},
"husky": {
"hooks": {
@@ -25,24 +26,35 @@
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
- "@tanstack/react-query": "^5.51.24",
+ "@tanstack/react-query": "^5.52.2",
+ "firebase": "^10.13.0",
+ "lucide-react": "^0.436.0",
+ "normalize.css": "^8.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hook-form": "^7.53.0",
+ "react-router-dom": "^6.26.1",
"zustand": "^4.5.5"
},
"devDependencies": {
+ "@emotion/eslint-plugin": "^11.12.0",
+ "@tanstack/eslint-plugin-query": "^5.52.0",
+ "@tanstack/react-query-devtools": "^5.52.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
+ "@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^7.15.0",
"@typescript-eslint/parser": "^7.15.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-emotion": "^11.0.0",
+ "eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
- "husky": "^8.0.3",
+ "husky": "^9.1.5",
"lint-staged": "^15.2.9",
"prettier": "^3.3.3",
"typescript": "^5.2.2",
diff --git a/public/vite.svg b/public/vite.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index b9d355df..00000000
--- a/src/App.css
+++ /dev/null
@@ -1,42 +0,0 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
-
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/src/App.tsx b/src/App.tsx
index b68a6838..a9976fbd 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,30 +1,18 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import router from '@/routes'
+import { RouterProvider } from 'react-router-dom'
+import GlobalStyles from '@/styles/GlobalStyles'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
-function App() {
- const [count, setCount] = useState(0)
+const queryClient = new QueryClient()
+const App = () => {
return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
- Click on the Vite and React logos to learn more
- >
+
+
+
+
+
)
}
diff --git a/src/assets/createlist_logo.svg b/src/assets/createlist_logo.svg
new file mode 100644
index 00000000..8b5cedf0
--- /dev/null
+++ b/src/assets/createlist_logo.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/assets/default-thumbnail.png b/src/assets/default-thumbnail.png
new file mode 100644
index 00000000..5ef9e373
Binary files /dev/null and b/src/assets/default-thumbnail.png differ
diff --git a/src/assets/myidoru_logo.svg b/src/assets/myidoru_logo.svg
new file mode 100644
index 00000000..ecb466e9
--- /dev/null
+++ b/src/assets/myidoru_logo.svg
@@ -0,0 +1,22 @@
+
diff --git a/src/assets/np_logo.svg b/src/assets/np_logo.svg
new file mode 100644
index 00000000..c497538e
--- /dev/null
+++ b/src/assets/np_logo.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/assets/react.svg b/src/assets/react.svg
deleted file mode 100644
index 6c87de9b..00000000
--- a/src/assets/react.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/src/components/Form.tsx b/src/components/Form.tsx
new file mode 100644
index 00000000..3674b2e5
--- /dev/null
+++ b/src/components/Form.tsx
@@ -0,0 +1,51 @@
+import { FormInputs, FormProps } from '@/types/formType'
+import { useForm } from 'react-hook-form'
+
+const Form: React.FC = ({ title, getDataForm, firebaseError }) => {
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ reset,
+ } = useForm({
+ mode: 'onChange',
+ })
+
+ const onSubmit = ({ email, password }: FormInputs) => {
+ getDataForm({ email, password })
+ reset()
+ }
+
+ const userEmail = {
+ required: '이메일을 입력해주세요.',
+ pattern: {
+ value: /\S+@\S+\.\S+/,
+ message: '이메일 형식이 올바르지 않습니다.',
+ },
+ }
+
+ const userPassword = {
+ required: '비밀번호를 입력해주세요.',
+ minLength: {
+ value: 6,
+ message: '비밀번호는 6자 이상이어야 합니다.',
+ },
+ }
+
+ return (
+
+ )
+}
+
+export default Form
diff --git a/src/components/comments/CommentCard.tsx b/src/components/comments/CommentCard.tsx
new file mode 100644
index 00000000..e3630814
--- /dev/null
+++ b/src/components/comments/CommentCard.tsx
@@ -0,0 +1,118 @@
+import React, { useState } from 'react'
+import styled from '@emotion/styled'
+import { Trash2 } from 'lucide-react'
+import { CommentType } from '@/types/commentType'
+import { colors } from '@/styles/colors'
+import { Link } from 'react-router-dom'
+import deleteComment from '@/service/comment/deleteComment'
+import { useIsMyProfile } from '@/hooks/useIsMyProfile'
+import Avatar from '@/components/common/Avatar'
+
+interface CommentCardProps {
+ comment: CommentType
+ playlistId: string
+ onCommentDeleted: () => void
+}
+
+const CommentCard: React.FC = ({ comment, playlistId, onCommentDeleted }) => {
+ const [isExpanded, setIsExpanded] = useState(false)
+ const { data: isCurrentUserComment } = useIsMyProfile(comment.userId)
+
+ const handleDeleteComment = async () => {
+ if (window.confirm('삭제하시겠습니까?')) {
+ await deleteComment(playlistId, comment.id)
+ onCommentDeleted()
+ }
+ }
+
+ const toggleCommentExpansion = () => {
+ setIsExpanded(!isExpanded)
+ }
+
+ return (
+
+
+
+ {/*
*/}
+
+
+
+
+ {comment.userName}
+
+ {comment.createdAt}
+ {isCurrentUserComment && (
+
+ )}
+
+
+ {comment.comment.length > 250 && !isExpanded ? (
+ <>
+ {comment.comment.slice(0, 250)} ···
+
+ 댓글 더보기
+
+ >
+ ) : (
+ comment.comment
+ )}
+ {isExpanded && (
+
+ 접기
+
+ )}
+
+
+
+ )
+}
+
+export default CommentCard
+
+const CardContainer = styled.div`
+ display: flex;
+ gap: 15px;
+ padding: 5px;
+ padding-bottom: 15px;
+
+ .comment-content {
+ flex-grow: 1;
+ }
+
+ .comment-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 5px;
+ }
+
+ .user-name {
+ font-weight: bold;
+ }
+
+ .comment-date {
+ font-size: 0.8em;
+ color: ${colors.darkGray};
+ }
+
+ .delete-button {
+ background: none;
+ border: none;
+ height: 14px;
+ cursor: pointer;
+ color: ${colors.lightGray};
+ }
+
+ .comment-text {
+ margin: 0;
+ word-break: break-word;
+ }
+
+ .expand-button {
+ color: ${colors.lightGray};
+ cursor: pointer;
+ margin-left: 5px;
+ }
+`
diff --git a/src/components/comments/CommentForm.tsx b/src/components/comments/CommentForm.tsx
new file mode 100644
index 00000000..2489a725
--- /dev/null
+++ b/src/components/comments/CommentForm.tsx
@@ -0,0 +1,92 @@
+import React, { useState } from 'react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { MESSAGES } from '@/constants/messages'
+import createComment from '@/service/comment/createComment'
+
+interface CommentFormProps {
+ playlistId: string
+ onCommentAdded: () => void
+}
+
+const CommentForm: React.FC = ({ playlistId, onCommentAdded }) => {
+ const [comment, setComment] = useState('')
+ const [warning, setWarning] = useState('')
+
+ const handleComment = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (comment.trim()) {
+ await createComment(playlistId, comment)
+ setComment('')
+ onCommentAdded()
+ }
+ }
+
+ const handleCommentChange = (e: React.ChangeEvent) => {
+ const newComment = e.target.value
+ if (newComment.length >= 400) {
+ setWarning(MESSAGES.COMMNET_OVER)
+ } else {
+ setWarning('')
+ setComment(newComment)
+ }
+ }
+
+ return (
+
+
+ {warning && {warning}
}
+
+ )
+}
+
+export default CommentForm
+
+const FormContainer = styled.div`
+ .comment-form {
+ display: flex;
+ margin-bottom: 20px;
+ }
+
+ .comment-form textarea {
+ flex-grow: 1;
+ padding: 10px;
+ height: 40px;
+ border: none;
+ border-bottom: 1px solid ${colors.lightGray};
+ resize: none;
+
+ &:focus {
+ outline: none;
+ }
+
+ ::placeholder {
+ color: ${colors.gray};
+ }
+ }
+
+ .comment-form button {
+ height: 40px;
+ margin-left: 10px;
+ padding: 0 10px;
+ background-color: ${colors.primaryPurple};
+ color: white;
+ border: none;
+ border-radius: 15px;
+ cursor: pointer;
+ }
+
+ .warning-text {
+ color: red;
+ margin-top: -10px;
+ margin-bottom: 10px;
+ }
+`
diff --git a/src/components/comments/Comments.tsx b/src/components/comments/Comments.tsx
new file mode 100644
index 00000000..94a43520
--- /dev/null
+++ b/src/components/comments/Comments.tsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect, useRef } from 'react'
+import { useQueryClient } from '@tanstack/react-query'
+import CommentForm from './CommentForm'
+import getPaginatedCommentsByPlaylistId from '@/service/comment/getPaginatedCommentsByPlaylistId'
+import CommentCard from '@/components/comments/CommentCard'
+import { CommentType } from '@/types/commentType'
+import { QueryDocumentSnapshot, DocumentData } from 'firebase/firestore'
+
+const COMMENTS_QUERY_KEY = 'comments'
+
+interface CommentsProps {
+ playlistId: string
+}
+
+const Comments: React.FC = ({ playlistId }) => {
+ const queryClient = useQueryClient()
+ const [comments, setComments] = useState([])
+ const [lastDoc, setLastDoc] = useState | null>(null)
+ const [isFetching, setIsFetching] = useState(false)
+ const [hasMore, setHasMore] = useState(true)
+ const [trigger, setTrigger] = useState(0)
+ const observerRef = useRef(null)
+
+ const loadMoreComments = async () => {
+ if (!hasMore) return
+
+ setIsFetching(true)
+
+ const { comments: newComments, lastDoc: newLastDoc } = await getPaginatedCommentsByPlaylistId(
+ playlistId,
+ lastDoc
+ )
+
+ setComments((prevComments) => {
+ const mergedComments = [...prevComments, ...newComments]
+
+ const uniqueComments = mergedComments.filter(
+ (comment, index, self) => index === self.findIndex((c) => c.id === comment.id)
+ )
+ return uniqueComments
+ })
+
+ if (newLastDoc) {
+ setLastDoc(newLastDoc)
+ } else {
+ setHasMore(false)
+ }
+
+ setIsFetching(false)
+ }
+
+ const invalidateComments = () => {
+ queryClient.invalidateQueries({ queryKey: [COMMENTS_QUERY_KEY, playlistId] })
+ setComments([])
+ setLastDoc(null)
+ setHasMore(true)
+ setTrigger((trigger) => trigger + 1)
+ }
+
+ useEffect(() => {
+ loadMoreComments()
+ }, [trigger])
+
+ useEffect(() => {
+ const observer = new IntersectionObserver(
+ (entries) => {
+ if (entries[0].isIntersecting && !isFetching && hasMore) {
+ loadMoreComments()
+ }
+ },
+ { threshold: 1 }
+ )
+ if (observerRef.current) {
+ observer.observe(observerRef.current)
+ }
+ return () => {
+ if (observerRef.current) {
+ observer.unobserve(observerRef.current)
+ }
+ }
+ }, [isFetching, lastDoc, hasMore])
+
+ return (
+
+
+
+
+ {comments.map((comment: CommentType) => (
+
+ ))}
+
+
+ {isFetching &&
Loading more comments...
}
+
+
+ )
+}
+
+export default Comments
diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx
new file mode 100644
index 00000000..657a6b75
--- /dev/null
+++ b/src/components/common/Avatar.tsx
@@ -0,0 +1,40 @@
+import styled from '@emotion/styled'
+import NPProfile from '@/assets/np_logo.svg'
+
+type AvatarSize = 'small' | 'large'
+
+interface AvatarProps {
+ src?: string
+ size?: AvatarSize
+}
+
+const Avatar = ({ src, size = 'small' }: AvatarProps) => {
+ return (
+
+
+
+ )
+}
+
+export default Avatar
+
+const sizeStyles = {
+ small: '34px',
+ large: '52px',
+}
+
+const AvatarComponent = styled.div<{ size: AvatarSize }>`
+ width: ${({ size }) => sizeStyles[size]};
+ height: ${({ size }) => sizeStyles[size]};
+ border-radius: 50%;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+`
diff --git a/src/components/common/Button/Button.tsx b/src/components/common/Button/Button.tsx
new file mode 100644
index 00000000..ae6a2552
--- /dev/null
+++ b/src/components/common/Button/Button.tsx
@@ -0,0 +1,123 @@
+import { CSSProperties, FC, ReactNode } from 'react'
+import { css } from '@emotion/react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { fontSize, fontWeight } from '@/styles/font'
+
+type ButtonSize = 'small' | 'normal'
+type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text'
+
+export interface ButtonProps {
+ children: ReactNode
+ onClick?: (event: React.MouseEvent) => void
+ size?: ButtonSize
+ variant?: ButtonVariant
+ buttonWidth?: CSSProperties['width']
+ disabled?: boolean
+}
+
+type ButtonComponentProps = Pick
+
+const Button: FC = ({
+ children,
+ onClick,
+ size = 'normal',
+ variant = 'primary',
+ buttonWidth = '100%',
+ disabled = false,
+}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default Button
+
+const baseStyles = css`
+ font-weight: ${fontWeight.medium};
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ text-align: center;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+`
+
+const themeColors: Record = {
+ primary: colors.primaryPurple,
+ secondary: colors.brightGray,
+ outline: colors.white,
+ text: 'transparent',
+}
+
+const themeTextColors: Record = {
+ primary: colors.white,
+ secondary: colors.darkGray,
+ outline: colors.darkGray,
+ text: colors.darkGray,
+}
+
+const themeHoverColors: Record = {
+ primary: colors.primaryPurpleHover,
+ secondary: colors.brightGrayHover,
+ outline: colors.lightestGray,
+ text: colors.primaryPurpleHover,
+}
+
+const sizeStyles = {
+ small: {
+ height: '30px',
+ padding: '6px',
+ fontSize: `${fontSize.sm}`,
+ },
+ normal: {
+ height: '50px',
+ padding: '16px',
+ fontSize: `${fontSize.md}`,
+ },
+}
+
+const ButtonComponent = styled.button`
+ ${baseStyles};
+ width: ${({ buttonWidth }) => buttonWidth};
+ height: ${({ size = 'normal' }) => sizeStyles[size].height};
+ padding: ${({ size = 'normal' }) => sizeStyles[size].padding};
+ background-color: ${({ variant = 'primary' }) => themeColors[variant]};
+ color: ${({ variant = 'primary' }) => themeTextColors[variant]};
+ border: ${({ variant }) => (variant === 'outline' ? `1px solid ${colors.black}` : 'none')};
+ font-weight: ${({ variant }) => (variant === 'primary' ? fontWeight.bold : fontWeight.medium)};
+ font-size: ${({ size = 'normal' }) => sizeStyles[size].fontSize};
+ transition:
+ background-color 0.2s ease,
+ color 0.2s ease;
+
+ &:hover {
+ cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
+ ${({ variant }) =>
+ variant === 'text' &&
+ `
+ color: ${themeHoverColors.text};
+ `}
+ ${({ variant = 'primary', disabled }) =>
+ variant !== 'text' &&
+ !disabled &&
+ `
+ background-color: ${themeHoverColors[variant]};
+ `}
+ }
+
+ &:disabled {
+ pointer-events: none;
+ opacity: 0.4;
+ }
+`
diff --git a/src/components/common/Button/ButtonImage.tsx b/src/components/common/Button/ButtonImage.tsx
new file mode 100644
index 00000000..c4f4e5f3
--- /dev/null
+++ b/src/components/common/Button/ButtonImage.tsx
@@ -0,0 +1,8 @@
+import styled from '@emotion/styled'
+
+const ButtonImage = styled.img`
+ margin-right: 8px;
+ height: 20px;
+`
+
+export default ButtonImage
diff --git a/src/components/common/Button/ButtonLink.tsx b/src/components/common/Button/ButtonLink.tsx
new file mode 100644
index 00000000..3ca150f2
--- /dev/null
+++ b/src/components/common/Button/ButtonLink.tsx
@@ -0,0 +1,130 @@
+import { CSSProperties, FC, ReactNode } from 'react'
+import { css } from '@emotion/react'
+import styled from '@emotion/styled'
+import { Link } from 'react-router-dom'
+import { colors } from '@/styles/colors'
+import { fontSize, fontWeight } from '@/styles/font'
+
+type ButtonSize = 'small' | 'normal'
+type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'text'
+
+export interface ButtonLinkProps {
+ children: ReactNode
+ to: string
+ size?: ButtonSize
+ variant?: ButtonVariant
+ buttonWidth?: CSSProperties['width']
+ disabled?: boolean
+}
+
+type ButtonLinkComponentProps = Pick<
+ ButtonLinkProps,
+ 'size' | 'variant' | 'buttonWidth' | 'disabled'
+>
+
+const ButtonLink: FC = ({
+ children,
+ to,
+ size = 'normal',
+ variant = 'primary',
+ buttonWidth = '100%',
+ disabled = false,
+}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export default ButtonLink
+
+const baseStyles = css`
+ font-weight: ${fontWeight.medium};
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ text-align: center;
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+`
+
+const themeColors: Record = {
+ primary: colors.primaryPurple,
+ secondary: colors.brightGray,
+ outline: colors.white,
+ text: 'transparent',
+}
+
+const themeTextColors: Record = {
+ primary: colors.white,
+ secondary: colors.darkGray,
+ outline: colors.darkGray,
+ text: colors.darkGray,
+}
+
+const themeHoverColors: Record = {
+ primary: colors.primaryPurpleHover,
+ secondary: colors.brightGrayHover,
+ outline: colors.lightestGray,
+ text: colors.primaryPurpleHover,
+}
+
+const sizeStyles = {
+ small: {
+ height: '30px',
+ padding: '6px',
+ fontSize: `${fontSize.sm}`,
+ },
+ normal: {
+ height: '50px',
+ padding: '16px',
+ fontSize: `${fontSize.md}`,
+ },
+}
+
+const ButtonLinkComponent = styled(Link, {
+ shouldForwardProp: (prop) =>
+ prop !== 'size' && prop !== 'variant' && prop !== 'buttonWidth' && prop !== 'disabled',
+})`
+ ${baseStyles};
+ width: ${({ buttonWidth }) => buttonWidth};
+ height: ${({ size = 'normal' }) => sizeStyles[size].height};
+ padding: ${({ size = 'normal' }) => sizeStyles[size].padding};
+ background-color: ${({ variant = 'primary' }) => themeColors[variant]};
+ color: ${({ variant = 'primary' }) => themeTextColors[variant]};
+ border: ${({ variant }) => (variant === 'outline' ? `1px solid ${colors.black}` : 'none')};
+ font-weight: ${({ variant }) => (variant === 'primary' ? fontWeight.bold : fontWeight.medium)};
+ font-size: ${({ size = 'normal' }) => sizeStyles[size].fontSize};
+ transition:
+ background-color 0.2s ease,
+ color 0.2s ease;
+
+ &:hover {
+ cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
+ ${({ variant }) =>
+ variant === 'text' &&
+ `
+ color: ${themeHoverColors.text};
+ `}
+ ${({ variant = 'primary', disabled }) =>
+ variant !== 'text' &&
+ !disabled &&
+ `
+ background-color: ${themeHoverColors[variant]};
+ `}
+ }
+
+ &:disabled {
+ pointer-events: none;
+ opacity: 0.4;
+ }
+`
diff --git a/src/components/common/Button/LikeButton.tsx b/src/components/common/Button/LikeButton.tsx
new file mode 100644
index 00000000..2550dae4
--- /dev/null
+++ b/src/components/common/Button/LikeButton.tsx
@@ -0,0 +1,49 @@
+import { Heart } from 'lucide-react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { useLikeButton } from '@/service/playlist/likePlaylist'
+
+type StyledHeartProps = {
+ isLiked: boolean
+}
+type StyledCountProps = {
+ isLiked: boolean
+}
+
+const LikeButton = ({ playlistId }: { playlistId: string }) => {
+ const { isLiked, likeCount, toggleLike } = useLikeButton(playlistId)
+
+ return (
+
+
+
+ {likeCount}
+
+
+ )
+}
+
+export default LikeButton
+
+const Container = styled.div`
+ .like-button {
+ cursor: pointer;
+ &:active {
+ transform: scale(1.2);
+ }
+ }
+ .like-count {
+ font-size: 0.8em;
+ display: inline-block;
+ }
+`
+
+const StyledHeart = styled(Heart, {
+ shouldForwardProp: (prop) => prop !== 'isLiked',
+})`
+ color: ${(props) => (props.isLiked ? colors.red : colors.gray)};
+`
+
+const StyledCount = styled.div`
+ color: ${(props) => (props.isLiked ? colors.red : colors.gray)};
+`
diff --git a/src/components/common/ImageGrid.tsx b/src/components/common/ImageGrid.tsx
new file mode 100644
index 00000000..53cf110f
--- /dev/null
+++ b/src/components/common/ImageGrid.tsx
@@ -0,0 +1,82 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import defaultThumbnail from '@/assets/default-thumbnail.png'
+
+type ImageGridSize = 'small' | 'large'
+
+interface ImageGridProps {
+ thumbnails: string[]
+ size?: ImageGridSize
+}
+
+const ImageGrid: React.FC = ({ thumbnails, size = 'large' }) => {
+ const displayThumbnails = [...thumbnails]
+ if (thumbnails.length === 3) {
+ displayThumbnails.push(defaultThumbnail)
+ }
+
+ return (
+
+ {displayThumbnails.map((thumbnail, index) => (
+
+

+
+ ))}
+
+ )
+}
+
+export default ImageGrid
+
+const sizeStyles = {
+ large: '70px',
+ small: '52px',
+}
+
+const Container = styled.div<{ count: number; size: ImageGridSize }>`
+ position: relative;
+ width: ${({ size }) => sizeStyles[size]};
+ height: ${({ size }) => sizeStyles[size]};
+ aspect-ratio: 1 / 1;
+ display: grid;
+ grid-template-columns: repeat(${(props) => Math.min(props.count, 2)}, 1fr);
+ border-radius: 10px;
+ overflow: hidden;
+
+ .play-button {
+ z-index: 2;
+ position: absolute;
+ bottom: 15px;
+ left: 10px;
+ width: 45px;
+ height: 45px;
+ background-color: rgba(128, 128, 128, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .triangle {
+ width: 0;
+ height: 0;
+ border-left: 10px solid white;
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ }
+ }
+
+ .image-container {
+ position: relative;
+ width: 100%;
+ padding-top: 100%;
+
+ img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+`
diff --git a/src/components/common/Input/Input.tsx b/src/components/common/Input/Input.tsx
new file mode 100644
index 00000000..35fb1ed6
--- /dev/null
+++ b/src/components/common/Input/Input.tsx
@@ -0,0 +1,82 @@
+import React, { forwardRef } from 'react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { CSSProperties } from 'react'
+
+export interface InputProps {
+ value: string
+ onChange: (event: React.ChangeEvent) => void
+ onKeyDown?: (event: React.KeyboardEvent) => void
+ name?: string
+ inputWidth?: CSSProperties['width']
+ placeholder?: string
+ type?: 'text' | 'password' | 'date'
+ disabled?: boolean
+ className?: string
+}
+
+type InputComponentProps = Pick
+
+const Input = React.memo(
+ forwardRef(
+ (
+ {
+ value,
+ onChange,
+ onKeyDown,
+ name,
+ inputWidth = '100%',
+ placeholder = '',
+ type = 'text',
+ disabled = false,
+ className,
+ },
+ ref
+ ) => (
+
+ )
+ )
+)
+
+const InputComponent = styled.input`
+ width: ${(props) => props.inputWidth};
+ height: 50px;
+ padding: 12px;
+ border: 1px solid ${colors.lightGray};
+ border-radius: 8px;
+ color: ${colors.black};
+
+ ::placeholder {
+ color: ${colors.gray};
+ }
+
+ &:hover {
+ background-color: ${colors.lightestGray};
+ }
+
+ &:focus {
+ outline: none;
+ border: 1px solid ${colors.primaryPurple};
+ }
+
+ &:disabled {
+ border-color: ${colors.lightestGray};
+ background-color: ${colors.lightestGray};
+ color: ${colors.black};
+ }
+`
+
+export default Input
+
+Input.displayName = 'Input'
diff --git a/src/components/common/Input/LineInput.tsx b/src/components/common/Input/LineInput.tsx
new file mode 100644
index 00000000..d9886bfd
--- /dev/null
+++ b/src/components/common/Input/LineInput.tsx
@@ -0,0 +1,43 @@
+import React, { forwardRef } from 'react'
+import styled from '@emotion/styled'
+import { InputProps } from './Input'
+import { colors } from '@/styles/colors'
+
+export interface LineInputProps extends InputProps {
+ lineType?: 'thin' | 'thick' | 'none'
+}
+
+const LineInput = React.memo(
+ forwardRef(({ lineType = 'none', ...rest }, ref) => (
+
+ ))
+)
+
+const LineInputComponent = styled.input<{ lineType?: 'thin' | 'thick' | 'none' }>`
+ height: 50px;
+ padding: 0 !important;
+ border: none !important;
+ border-bottom: ${({ lineType }) =>
+ lineType === 'thin' ? '2px' : lineType === 'thick' ? '3px' : '0px'}
+ solid ${colors.lightGray} !important;
+ border-radius: 0 !important;
+ color: ${colors.black};
+
+ ::placeholder {
+ color: ${colors.gray};
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:disabled {
+ border-color: ${colors.lightestGray};
+ background-color: ${colors.lightestGray};
+ color: ${colors.black};
+ }
+`
+
+export default LineInput
+
+LineInput.displayName = 'LineInput'
diff --git a/src/components/common/Loading.tsx b/src/components/common/Loading.tsx
new file mode 100644
index 00000000..0c4e609d
--- /dev/null
+++ b/src/components/common/Loading.tsx
@@ -0,0 +1,52 @@
+import { colors } from '@/styles/colors'
+import styled from '@emotion/styled'
+import NPProfile from '@/assets/np_logo.svg'
+
+const Loading = () => {
+ return (
+
+
+
+

+
+
+ )
+}
+
+export default Loading
+
+const LoadingContainer = styled.div`
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: calc(100vh - 104px);
+
+ .spinner {
+ width: 80px;
+ height: 80px;
+ border: 5px solid ${colors.lightGray};
+ border-top: 5px solid ${colors.primaryPurple};
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ }
+
+ .logo {
+ position: absolute;
+ height: 36px;
+
+ img {
+ height: 100%;
+ }
+ }
+
+ @keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+`
diff --git a/src/components/layout/Navbar.tsx b/src/components/layout/Navbar.tsx
new file mode 100644
index 00000000..b0dec37e
--- /dev/null
+++ b/src/components/layout/Navbar.tsx
@@ -0,0 +1,109 @@
+import styled from '@emotion/styled'
+import { NavLink } from 'react-router-dom'
+import { PATH } from '@/constants/path'
+import { CircleUserRound, House, MessageCircleMore, Search, SquarePlus } from 'lucide-react'
+import { colors } from '@/styles/colors'
+import { useState, useEffect } from 'react'
+import { getLoggedInUserUID, getUserIdFromUID } from '@/utils/userDataUtils'
+
+interface IconLinks {
+ path: string
+ icon: JSX.Element
+}
+
+const Navbar = () => {
+ const uid = getLoggedInUserUID()
+ const [userId, setUserId] = useState(null)
+
+ useEffect(() => {
+ const fetchUserId = async () => {
+ if (uid) {
+ try {
+ const id = await getUserIdFromUID(uid)
+ setUserId(id)
+ } catch (error) {
+ console.error('Failed to fetch user ID:', error)
+ }
+ }
+ }
+ fetchUserId()
+ }, [uid, userId])
+
+ const menu: Array = [
+ { path: PATH.HOME, icon: },
+ { path: PATH.SEARCH, icon: },
+ { path: PATH.CREATEPLAYLIST, icon: },
+ { path: PATH.CHAT, icon: },
+ { path: `profile/${userId}`, icon: },
+ ]
+
+ return (
+
+ {menu.map(({ path, icon }) => (
+ (isActive ? 'nav-link-active' : 'nav-link')}
+ >
+ {icon}
+
+ ))}
+
+ )
+}
+
+export default Navbar
+
+const StyledMenuContainer = styled.nav`
+ z-index: 99;
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+
+ display: flex;
+ justify-content: space-around;
+ width: 100%;
+ max-width: 430px;
+ margin: 0 auto;
+ height: 52px;
+ border-top: 1px solid ${colors.gray};
+ background-color: ${colors.white};
+
+ .nav-link-active,
+ .nav-link {
+ padding-bottom: 5px;
+ width: calc(100% / 5);
+ height: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ /* border: 1px solid ${colors.red}; */
+ }
+
+ .nav-link {
+ color: ${colors.gray};
+ }
+`
+
+// const StyledMenuItem = styled.div`
+// width: calc(100% / 5);
+// height: 100%;
+// display: flex;
+// justify-content: center;
+// align-items: center;
+// background-color: lime;
+
+// .nav-link-active,
+// .nav-link {
+// width: calc(100% / 5);
+// height: 100%;
+// display: flex;
+// justify-content: center;
+// background-color: ${colors.red};
+// }
+
+// .nav-link {
+// color: ${colors.gray};
+// }
+// `
diff --git a/src/components/layout/header/BackHeader.tsx b/src/components/layout/header/BackHeader.tsx
new file mode 100644
index 00000000..9e51662f
--- /dev/null
+++ b/src/components/layout/header/BackHeader.tsx
@@ -0,0 +1,37 @@
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { ArrowLeft } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+
+const BackHeader = () => {
+ const navigate = useNavigate()
+ const handleBack = () => {
+ navigate(-1)
+ }
+ return (
+
+
+
+ )
+}
+
+export default BackHeader
+
+const Container = styled.div`
+ width: 100%;
+ max-width: 430px;
+ height: 52px;
+
+ .logo-myidoru {
+ width: 114px;
+ margin: 10px 20px 0;
+ }
+ .button-back {
+ background-color: ${colors.white};
+ border: none;
+ cursor: pointer;
+ margin: 16px;
+ }
+`
diff --git a/src/components/layout/header/Header.tsx b/src/components/layout/header/Header.tsx
new file mode 100644
index 00000000..cd7fe69f
--- /dev/null
+++ b/src/components/layout/header/Header.tsx
@@ -0,0 +1,56 @@
+import styled from '@emotion/styled'
+import { useLocation, Link, useParams } from 'react-router-dom'
+import logo from '@/assets/myidoru_logo.svg'
+import { PATH } from '@/constants/path'
+import { colors } from '@/styles/colors'
+import BackHeader from '@/components/layout/header/BackHeader'
+import LogoutHeader from '@/components/layout/header/LogoutHeader'
+import { useIsMyProfile } from '@/hooks/useIsMyProfile'
+
+const Header = () => {
+ const location = useLocation()
+ const { userId } = useParams<{ userId: string }>()
+ const { data: isMyProfile } = useIsMyProfile(userId)
+ const pathDepth = location.pathname.split('/').filter(Boolean).length
+ const isUserProfilePath = /^\/profile\/[^/]+$/.test(location.pathname)
+ const isPlaylistPath = /^\/playlist\/[^/]+$/.test(location.pathname)
+
+ return (
+ <>
+ {pathDepth < 2 || (!isUserProfilePath && !isPlaylistPath) ? (
+
+
+
+
+
+ ) : isPlaylistPath ? (
+
+ ) : isUserProfilePath ? (
+ isMyProfile ? (
+
+ ) : (
+
+ )
+ ) : null}
+ >
+ )
+}
+
+export default Header
+
+const Container = styled.div`
+ width: 100%;
+ max-width: 430px;
+ height: 52px;
+
+ .logo-myidoru {
+ width: 114px;
+ margin: 10px 20px 0;
+ }
+ .button-back {
+ background-color: ${colors.white};
+ border: none;
+ cursor: pointer;
+ margin: 16px;
+ }
+`
diff --git a/src/components/layout/header/LogoutHeader.tsx b/src/components/layout/header/LogoutHeader.tsx
new file mode 100644
index 00000000..0fcdc8ce
--- /dev/null
+++ b/src/components/layout/header/LogoutHeader.tsx
@@ -0,0 +1,50 @@
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { LogOut } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+import logout from '@/service/auth/logout'
+import { fontSize } from '@/styles/font'
+import { fontWeight } from '@/styles/font'
+
+interface LogoutHeaderProps {
+ userId: string
+}
+
+const LogoutHeader = ({ userId }: LogoutHeaderProps) => {
+ const navigate = useNavigate()
+
+ const logoutBtnHandler = async () => {
+ const isLogoutSuccess = await logout()
+ if (isLogoutSuccess) navigate('/login')
+ }
+
+ return (
+
+ {userId}
+
+
+ )
+}
+
+export default LogoutHeader
+
+const Container = styled.div`
+ width: 100%;
+ max-width: 430px;
+ height: 60px;
+ display: flex;
+ justify-content: space-between;
+ padding: 0 20px;
+ align-items: center;
+ font-size: ${fontSize.lg};
+ font-weight: ${fontWeight.bold};
+
+ .button-logout {
+ background-color: ${colors.white};
+ border: none;
+ cursor: pointer;
+ margin: 16px;
+ }
+`
diff --git a/src/components/playlist/MusicItem.tsx b/src/components/playlist/MusicItem.tsx
new file mode 100644
index 00000000..2cdb577f
--- /dev/null
+++ b/src/components/playlist/MusicItem.tsx
@@ -0,0 +1,175 @@
+import styled from '@emotion/styled'
+import { BasicVideoProps, PlaylistBaseProps } from '@/types/playlistType'
+import { Trash2 } from 'lucide-react'
+import { colors } from '@/styles/colors'
+import { fontSize } from '@/styles/font'
+import { Link } from 'react-router-dom'
+import ImageGrid from '@/components/common/ImageGrid'
+
+interface MusicItemProps {
+ videoList: BasicVideoProps[] | PlaylistBaseProps[]
+ onItemDelete?: (id: number) => void | ''
+ variant: 'profilePL' | 'createPL'
+}
+
+const isVideoListProps = (item: PlaylistBaseProps | BasicVideoProps): item is BasicVideoProps => {
+ return 'channelTitle' in item
+}
+
+const MusicItem = ({ videoList, onItemDelete, variant }: MusicItemProps) => {
+ return (
+
+ {videoList.map((item, idx) => {
+ const thumbnails = Array.isArray(item.thumbnail) ? item.thumbnail : [item.thumbnail]
+
+ return (
+
+ {variant === 'profilePL' && 'playlistId' in item ? (
+
+
+
+
+
+
{item.title}
+ {item.isPrivate &&
비공개
}
+
+
+ ) : (
+ <>
+
+
+
+
+
+
{item.title}
+ {isVideoListProps(item) &&
{item.channelTitle}}
+
+
+
+ >
+ )}
+
+ )
+ })}
+
+ )
+}
+
+export default MusicItem
+
+export const Container = styled.div<{ variant: 'profilePL' | 'createPL'; isPrivate?: boolean }>`
+ .item-video {
+ display: flex;
+ align-items: center;
+ padding: ${(props) => (props.variant === 'createPL' ? '10px' : '5px 0')};
+ border-radius: ${(props) => (props.variant === 'createPL' ? '5px' : '0')};
+ background-color: ${(props) => (props.variant === 'createPL' ? colors.white : 'transparent')};
+ justify-content: space-between;
+
+ a {
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: inherit;
+ width: 100%;
+ }
+
+ &:hover {
+ background-color: ${(props) =>
+ props.variant === 'profilePL' ? colors.brightGray : colors.white};
+ }
+ }
+
+ .content-container {
+ display: flex;
+ }
+
+ .thumbnail-list {
+ padding: 5px 20px;
+ }
+
+ .thumbnail {
+ max-width: ${(props) => (props.variant === 'profilePL' ? '60px' : '0')};
+ max-height: ${(props) => (props.variant === 'profilePL' ? '60px' : '0')};
+ display: ${(props) => (props.variant === 'profilePL' ? 'flex' : 'none')};
+ align-items: center;
+ justify-content: center;
+ margin-right: 10px;
+ }
+
+ .item-video img {
+ width: ${(props) => (props.variant === 'createPL' ? '120px' : 'transparent')};
+ height: ${(props) => (props.variant === 'createPL' ? '90px' : 'transparent')};
+ object-fit: cover;
+ margin-right: ${(props) => (props.variant === 'createPL' ? '14px' : '10px')};
+ display: block;
+ }
+
+ .thumbnail-img {
+ margin-right: 16px;
+ }
+
+ .video-thumbnail {
+ width: 100px;
+ }
+
+ .text-content {
+ display: flex;
+ flex-direction: column;
+ width: ${(props) => (props.variant === 'createPL' ? '210px' : '100%')};
+ margin-right: 14px;
+ line-height: 20px;
+
+ @media (max-width: 390px) {
+ width: ${(props) => (props.variant === 'profilePL' ? '280px' : 'auto')};
+ }
+
+ .tag-private {
+ background-color: ${colors.lightPurPle};
+ color: ${colors.primaryPurple};
+ font-size: ${fontSize.sm};
+ width: 54px;
+ border-radius: 15px;
+ text-align: center;
+ }
+ }
+
+ .text-content p {
+ font-size: ${fontSize.md};
+ font-weight: bold;
+ -webkit-line-clamp: ${({ isPrivate }) => (isPrivate ? '1' : '2')};
+ display: -webkit-box;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ word-break: break-word;
+ -webkit-box-orient: vertical;
+ margin: 0;
+ color: ${colors.black};
+ }
+
+ .text-content span {
+ font-size: ${fontSize.sm};
+ color: ${colors.gray};
+ margin-top: 4px;
+ }
+
+ .btn-delete {
+ width: ${(props) => (props.variant === 'createPL' ? '38px' : '24px')};
+ height: ${(props) => (props.variant === 'createPL' ? '37px' : '24px')};
+ border: 1px solid ${colors.lightestGray};
+ border-radius: 50%;
+ padding: 5px;
+ cursor: pointer;
+ display: ${(props) => (props.variant === 'createPL' ? 'block' : 'none')};
+ }
+`
diff --git a/src/components/playlist/PlaylistThumbnailFeed.tsx b/src/components/playlist/PlaylistThumbnailFeed.tsx
new file mode 100644
index 00000000..5268eac2
--- /dev/null
+++ b/src/components/playlist/PlaylistThumbnailFeed.tsx
@@ -0,0 +1,76 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import defaultThumbnail from '@/assets/default-thumbnail.png'
+
+interface PlaylistThumbnailFeedProps {
+ thumbnails: string[]
+}
+
+const PlaylistThumbnailFeed: React.FC = ({ thumbnails = [] }) => {
+ const displayThumbnails = [...thumbnails]
+ if (thumbnails.length === 3) {
+ displayThumbnails.push(defaultThumbnail)
+ }
+
+ return (
+
+
+ {displayThumbnails.map((thumbnail, index) => (
+
+

+
+ ))}
+
+ )
+}
+
+export default PlaylistThumbnailFeed
+
+const Container = styled.div<{ count: number }>`
+ position: relative;
+ width: 100%;
+ aspect-ratio: 1 / 1;
+ display: grid;
+ grid-template-columns: repeat(${(props) => Math.min(props.count, 2)}, 1fr);
+ border-radius: 10px;
+ overflow: hidden;
+
+ .play-button {
+ z-index: 2;
+ position: absolute;
+ bottom: 15px;
+ left: 10px;
+ width: 45px;
+ height: 45px;
+ background-color: rgba(128, 128, 128, 0.5);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .triangle {
+ width: 0;
+ height: 0;
+ border-left: 10px solid white;
+ border-top: 6px solid transparent;
+ border-bottom: 6px solid transparent;
+ }
+ }
+
+ .image-container {
+ position: relative;
+ width: 100%;
+ padding-top: 100%;
+
+ img {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+`
diff --git a/src/components/playlist/TagList.tsx b/src/components/playlist/TagList.tsx
new file mode 100644
index 00000000..a09e15aa
--- /dev/null
+++ b/src/components/playlist/TagList.tsx
@@ -0,0 +1,63 @@
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { fontSize } from '@/styles/font'
+import { X } from 'lucide-react'
+
+interface TagListProps {
+ tags: string[]
+ onTagDelete: (id: number) => void
+}
+
+const TagList = ({ tags, onTagDelete }: TagListProps) => {
+ return (
+
+
+ {tags.map((t, idx) => (
+
+ {t}
+ onTagDelete(idx)}>
+
+
+
+ ))}
+
+
+ )
+}
+
+export default TagList
+
+const Container = styled.div`
+ .tag {
+ background-color: ${colors.lightPurPle};
+ color: ${colors.primaryPurple};
+ }
+
+ .btn-tagdelete {
+ border: none !important;
+ cursor: pointer;
+ margin-top: 4px;
+ margin-left: 2px;
+ }
+
+ .container-tag {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ max-width: 100%;
+ white-space: nowrap;
+ }
+
+ .tag {
+ border-radius: 20px;
+ padding: 5px 10px;
+ font-size: ${fontSize.sm};
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+
+ @media (min-width: 400px) {
+ font-size: ${fontSize.sm};
+ }
+ }
+`
diff --git a/src/components/playlist/UserInfo.tsx b/src/components/playlist/UserInfo.tsx
new file mode 100644
index 00000000..82596e6b
--- /dev/null
+++ b/src/components/playlist/UserInfo.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import styled from '@emotion/styled'
+import Avatar from '@/components/common/Avatar'
+import { fontSize, fontWeight } from '@/styles/font'
+import NPProfile from '@/assets/np_logo.svg'
+
+interface UserInfoProps {
+ className?: string
+ authorName?: string
+ createdAt?: string
+ authorImg?: string
+ onClick?: (event: React.MouseEvent) => void
+}
+
+const UserInfo: React.FC = ({
+ className,
+ authorName,
+ createdAt = 'Unknown Date',
+ authorImg,
+ onClick,
+}) => {
+ return (
+
+
+
+
{authorName || NPProfile}
+
{createdAt}
+
+
+ )
+}
+
+export default UserInfo
+
+const UserInfoComponent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ .user-container {
+ line-height: 1;
+
+ .user-name-text {
+ font-size: ${fontSize.sm};
+ font-weight: ${fontWeight.bold};
+ }
+
+ .user-posting-date {
+ font-size: ${fontSize.xs};
+ }
+ }
+`
diff --git a/src/components/search/PlaylistItem.tsx b/src/components/search/PlaylistItem.tsx
new file mode 100644
index 00000000..aadcf124
--- /dev/null
+++ b/src/components/search/PlaylistItem.tsx
@@ -0,0 +1,54 @@
+import { Link } from 'react-router-dom'
+import PlaylistThumbnails from '@/components/search/PlaylistThumbnails'
+import { Playlist } from '@/service/search/searchTag'
+import styled from '@emotion/styled'
+import { fontSize, fontWeight } from '@/styles/font'
+
+const PlaylistItem = ({ playlist }: { playlist: Playlist }) => {
+ return (
+
+
+
+
+ {playlist.title}
+
+
+
+ )
+}
+
+export default PlaylistItem
+const Container = styled.div`
+ .success-tag {
+ padding-top: 5%;
+ font-size: ${fontSize.xl};
+ font-weight: ${fontWeight.bold};
+ text-align: center;
+ margin-bottom: 20px;
+ }
+ .search-success {
+ margin: auto;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ gap: 10px;
+ }
+ .success-img {
+ display: flex;
+ justify-content: center;
+ }
+ .success-title {
+ padding: 10px 5px;
+ margin-bottom: 20px;
+ width: 100%;
+ height: 53px;
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ font-size: ${fontSize.md};
+ font-weight: ${fontWeight.medium};
+ }
+`
diff --git a/src/components/search/PlaylistThumbnails.tsx b/src/components/search/PlaylistThumbnails.tsx
new file mode 100644
index 00000000..b598a37c
--- /dev/null
+++ b/src/components/search/PlaylistThumbnails.tsx
@@ -0,0 +1,53 @@
+import { useState, useEffect } from 'react'
+import { ExtendedVideoProps } from '@/types/playlistType'
+import styled from '@emotion/styled'
+import getUserPlaylistDetails from '@/service/playlist/getUserPlaylistDetails'
+import defaultThumbnail from '@/assets/default-thumbnail.png'
+
+const PlaylistThumbnails = ({ playlistId }: { playlistId: string }) => {
+ const [videos, setVideos] = useState([])
+
+ useEffect(() => {
+ const fetchThumbnails = async () => {
+ const fetchedVideos = await getUserPlaylistDetails(playlistId)
+ setVideos(fetchedVideos)
+ }
+ fetchThumbnails()
+ }, [playlistId])
+
+ const thumbnails = videos.slice(0, 4)
+
+ const displayThumbnails: (ExtendedVideoProps | string)[] = [...thumbnails]
+ if (thumbnails.length === 3) {
+ displayThumbnails.push(defaultThumbnail)
+ }
+
+ return (
+
+ {displayThumbnails.map((video, idx) => (
+
+ ))}
+
+ )
+}
+
+export default PlaylistThumbnails
+
+const Container = styled.div<{ count: number }>`
+ width: 100%;
+ max-width: 180px;
+ aspect-ratio: 1 / 1;
+ display: grid;
+ grid-template-columns: repeat(${(props) => Math.min(props.count, 2)}, 1fr);
+ border-radius: 15px;
+ overflow: hidden;
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+`
diff --git a/src/components/search/RecommendKeyword.tsx b/src/components/search/RecommendKeyword.tsx
new file mode 100644
index 00000000..a845f04f
--- /dev/null
+++ b/src/components/search/RecommendKeyword.tsx
@@ -0,0 +1,37 @@
+import React, { useEffect, useState } from 'react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import getRecommendTags from '@/service/search/getRecommendTags'
+
+interface RecommendedKeywordProps {
+ keyword: string
+ onClick: (keyword: string) => void
+}
+
+export const Keywords = () => {
+ const [tagkeywords, setTagkeywords] = useState([])
+ useEffect(() => {
+ const fetchTags = async () => {
+ const tags = await getRecommendTags()
+ setTagkeywords(tags)
+ }
+
+ fetchTags()
+ }, [])
+ return tagkeywords
+}
+
+export const RecommendedKeyword: React.FC = ({ keyword, onClick }) => {
+ return onClick(keyword)}>{keyword}
+}
+
+const Keyword = styled.p`
+ display: inline-block;
+ padding: 10px;
+ margin: 5px;
+ border: 1px solid ${colors.mediumPurple};
+ color: ${colors.white};
+ background-color: ${colors.mediumPurple};
+ border-radius: 20px;
+ cursor: pointer;
+`
diff --git a/src/components/search/RecommendSearch.tsx b/src/components/search/RecommendSearch.tsx
new file mode 100644
index 00000000..767471d6
--- /dev/null
+++ b/src/components/search/RecommendSearch.tsx
@@ -0,0 +1,32 @@
+import { RecommendedKeyword } from '@/components/search/RecommendKeyword'
+import styled from '@emotion/styled'
+
+type RecommendSearchProps = {
+ recommendedKeywords: string[]
+ setSearchTag: (tag: string) => void
+}
+
+const RecommendSearch = ({ recommendedKeywords, setSearchTag }: RecommendSearchProps) => (
+
+
+
추천 검색어
+
+ {recommendedKeywords.map((keyword) => (
+
+ ))}
+
+
+
+)
+
+export default RecommendSearch
+
+const Container = styled.div`
+ .recommend-search {
+ padding: 15% 3% 0 3%;
+ }
+ .recommend-keyword {
+ padding-top: 5%;
+ cursor: pointer;
+ }
+`
diff --git a/src/components/search/SearchFail.tsx b/src/components/search/SearchFail.tsx
new file mode 100644
index 00000000..48c9e1da
--- /dev/null
+++ b/src/components/search/SearchFail.tsx
@@ -0,0 +1,39 @@
+import styled from '@emotion/styled'
+import { fontSize, fontWeight } from '@/styles/font'
+
+const SearchFail = ({ previousSearchTag }: { previousSearchTag: string }) => (
+
+
+
검색 태그 : {previousSearchTag}
+
+ 죄송합니다.
+
+ "{previousSearchTag}" 에
+ 해당하는 플레이리스트를
+ 찾을 수 없습니다.
+
+
+
+)
+
+export default SearchFail
+const Container = styled.div`
+ .search-fail {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ height: calc(80vh - 100px);
+ min-height: 300px;
+ text-align: center;
+ }
+ .fail-title {
+ padding: 15% 0;
+ font-size: ${fontSize.xl};
+ }
+ .fail-text {
+ font-size: ${fontSize.lg};
+ font-weight: ${fontWeight.medium};
+ line-height: 2;
+ }
+`
diff --git a/src/components/search/SearchSuccess.tsx b/src/components/search/SearchSuccess.tsx
new file mode 100644
index 00000000..332ee164
--- /dev/null
+++ b/src/components/search/SearchSuccess.tsx
@@ -0,0 +1,41 @@
+import { Playlist } from '@/service/search/searchTag'
+import PlaylistItem from '@/components/search/PlaylistItem'
+import { fontSize, fontWeight } from '@/styles/font'
+import styled from '@emotion/styled'
+
+const SearchSuccess = ({
+ previousSearchTag,
+ playlists,
+}: {
+ previousSearchTag: string
+ playlists: Playlist[]
+}) => (
+
+
+ {!previousSearchTag.startsWith('#') ? `#${previousSearchTag}` : previousSearchTag}
+
+
+ {playlists.map((playlist) => (
+
+ ))}
+
+
+)
+
+export default SearchSuccess
+const Container = styled.div`
+ .success-tag {
+ padding-top: 5%;
+ font-size: ${fontSize.xl};
+ font-weight: ${fontWeight.bold};
+ text-align: center;
+ margin-bottom: 20px;
+ }
+ .search-success {
+ margin: auto;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: 1fr 1fr;
+ gap: 10px;
+ }
+`
diff --git a/src/constants/firebaseKeys.ts b/src/constants/firebaseKeys.ts
new file mode 100644
index 00000000..f367babe
--- /dev/null
+++ b/src/constants/firebaseKeys.ts
@@ -0,0 +1 @@
+export const FIREBASE_SESSION_KEY = `firebase:authUser:${import.meta.env.VITE_FIREBASE_API_KEY as string}:[DEFAULT]`
diff --git a/src/constants/messages.ts b/src/constants/messages.ts
new file mode 100644
index 00000000..6e996558
--- /dev/null
+++ b/src/constants/messages.ts
@@ -0,0 +1,20 @@
+import { MAX_TAG_COUNT, MAX_TAG_LENGTH } from '@/hooks/useTags'
+
+export const MESSAGES = {
+ LOGIN: {
+ FAIL: '아이디 또는 비밀번호를 입력해주세요.',
+ },
+ RESET_PASSWORD: {
+ SUCCESS: '비밀번호 재설정 링크가 이메일로 전송되었습니다.',
+ FAIL: '올바른 이메일 형식을 입력하세요.',
+ },
+ COMMNET_OVER: '댓글은 최대 400자까지 입력할 수 있습니다.',
+ CREATE_PL: {
+ TAG_FAIL: '유효하지 않은 태그입니다.',
+ TAG_SPACE_FAIL: '태그에는 공백을 포함할 수 없습니다.',
+ TAG_COUNT_FAIL: `태그는 최대 ${MAX_TAG_COUNT}개까지 입력 가능합니다.`,
+ TAG_LENGTH_FAIL: `태그는 최대 ${MAX_TAG_LENGTH - 1}자까지 가능합니다.`,
+ YOUTUBE: '유효한 유튜브 링크를 입력하세요.',
+ DUPLICATION: '중복된 링크입니다.',
+ },
+}
diff --git a/src/constants/path.ts b/src/constants/path.ts
new file mode 100644
index 00000000..8af592ed
--- /dev/null
+++ b/src/constants/path.ts
@@ -0,0 +1,14 @@
+export const PATH = {
+ HOME: '/',
+ SEARCH: '/search',
+ PAGE404: '*',
+ LOGIN: '/login',
+ SIGNUP: '/signup',
+ PLAYLIST: '/playlist/:playlistId',
+ CREATEPLAYLIST: '/createplaylist',
+ USER_PROFILE: '/profile/:userId',
+ EDITPROFILE: '/profile/editprofile',
+ FOLLOW: 'follow',
+ CHAT: '/chat',
+ EDITPW: '/login/editpassword',
+}
diff --git a/src/firebase/firebaseConfig.ts b/src/firebase/firebaseConfig.ts
new file mode 100644
index 00000000..c701a512
--- /dev/null
+++ b/src/firebase/firebaseConfig.ts
@@ -0,0 +1,18 @@
+import { initializeApp } from 'firebase/app'
+import { getAuth } from 'firebase/auth'
+import { Firestore, getFirestore } from 'firebase/firestore'
+
+const firebaseConfig = {
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
+ measurementId: import.meta.env.VITE_FIREBASE_MEASUREMENT_ID,
+ databaseURL: import.meta.env.VITE_FIREBASE_REALTIME_DB,
+}
+
+export const app = initializeApp(firebaseConfig)
+export const auth = getAuth(app)
+export const db: Firestore = getFirestore(app)
diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts
new file mode 100644
index 00000000..fe03276e
--- /dev/null
+++ b/src/hooks/auth/useAuth.ts
@@ -0,0 +1,16 @@
+import useAuthStore from '@/stores/useAuthStore'
+import { useEffect } from 'react'
+import { useAuthQuery } from './useAuthQuery'
+
+export const useAuth = () => {
+ const { setAuthenticated } = useAuthStore()
+ const { data: isAuthenticated, isLoading, error } = useAuthQuery()
+
+ useEffect(() => {
+ if (isAuthenticated !== undefined) {
+ setAuthenticated(isAuthenticated)
+ }
+ }, [isAuthenticated, setAuthenticated])
+
+ return { isAuthenticated, isLoading, error }
+}
diff --git a/src/hooks/auth/useAuthQuery.ts b/src/hooks/auth/useAuthQuery.ts
new file mode 100644
index 00000000..392ba781
--- /dev/null
+++ b/src/hooks/auth/useAuthQuery.ts
@@ -0,0 +1,9 @@
+import { useQuery } from '@tanstack/react-query'
+import checkAuth from '@/service/auth/checkAuth'
+
+export const useAuthQuery = () => {
+ return useQuery({
+ queryKey: ['authStatus'],
+ queryFn: checkAuth,
+ })
+}
diff --git a/src/hooks/useFollowStatus.ts b/src/hooks/useFollowStatus.ts
new file mode 100644
index 00000000..a6178b67
--- /dev/null
+++ b/src/hooks/useFollowStatus.ts
@@ -0,0 +1,47 @@
+import { useState, useEffect } from 'react'
+import { useMutation } from '@tanstack/react-query'
+import { followUser, followStatus } from '@/service/profile/followService'
+
+export const useFollowButton = (targetUserId: string, currentUID: string) => {
+ const [followData, setfollowData] = useState({
+ followers: [] as string[],
+ following: [] as string[],
+ })
+ const [isFollowing, setIsFollowing] = useState(false)
+
+ const followMutation = useMutation({
+ mutationFn: async () => {
+ await followUser(targetUserId, currentUID, isFollowing)
+ },
+ onMutate: async () => {
+ setIsFollowing((prev) => prev)
+ },
+ })
+
+ const toggleFollow = async () => {
+ await followMutation.mutateAsync()
+ }
+
+ useEffect(() => {
+ let unsubscribe: (() => void) | undefined
+ const fetchFollowStatus = async () => {
+ if (currentUID && targetUserId) {
+ unsubscribe = await followStatus(targetUserId, currentUID, (data, isFollowing) => {
+ setfollowData(data)
+ setIsFollowing(isFollowing)
+ })
+ }
+ }
+ fetchFollowStatus()
+ return () => {
+ if (unsubscribe) {
+ unsubscribe()
+ }
+ }
+ }, [currentUID, targetUserId])
+
+ const followerCount = followData.followers.length
+ const followingCount = followData.following.length
+
+ return { followData, isFollowing, toggleFollow, followerCount, followingCount }
+}
diff --git a/src/hooks/useIsMyProfile.ts b/src/hooks/useIsMyProfile.ts
new file mode 100644
index 00000000..0996d90d
--- /dev/null
+++ b/src/hooks/useIsMyProfile.ts
@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query'
+import { getLoggedInUserUID, getUIDFromUserId } from '@/utils/userDataUtils'
+
+const fetchProfileData = async (userId?: string) => {
+ const currentUser = await getLoggedInUserUID()
+ if (!userId) return false
+ const uidFromUserId = await getUIDFromUserId(userId)
+ return uidFromUserId === currentUser
+}
+
+export const useIsMyProfile = (userId?: string) => {
+ return useQuery({
+ queryKey: ['isMyProfile', userId],
+ queryFn: () => fetchProfileData(userId),
+ enabled: !!userId,
+ })
+}
diff --git a/src/hooks/usePLItem.ts b/src/hooks/usePLItem.ts
new file mode 100644
index 00000000..41912859
--- /dev/null
+++ b/src/hooks/usePLItem.ts
@@ -0,0 +1,79 @@
+import { BasicVideoProps } from '@/types/playlistType'
+import { useState } from 'react'
+
+const usePLItem = () => {
+ const [isValid, setIsValid] = useState(true)
+ const [validType, setValidType] = useState('')
+ const [videoList, setVideoList] = useState([])
+ const [url, setUrl] = useState('')
+
+ const getVideoIdFromUrl = (url: string): string | null => {
+ const regex =
+ /(?:youtube\.com\/(?:[^/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/
+ const match = url.match(regex)
+ return match ? match[1] : null
+ }
+
+ const handleAddList = async () => {
+ const isValidYoutubeUrl = (url: string): boolean => {
+ const regex = /^(https?:\/\/)?(www\.youtube\.com|youtu\.?be)\/.+$/
+ return regex.test(url)
+ }
+
+ const isUniqueUrl = (url: string): boolean => {
+ const videoId = getVideoIdFromUrl(url)
+ if (!videoId) return false
+ return !videoList.some((video) => getVideoIdFromUrl(video.url) === videoId)
+ }
+
+ if (!isValidYoutubeUrl(url)) {
+ setIsValid(false)
+ setValidType('Youtube')
+ return
+ } else if (!isUniqueUrl(url)) {
+ setIsValid(false)
+ setValidType('Duplication')
+ return
+ }
+ setIsValid(true)
+
+ const youtubeKey = import.meta.env.VITE_YOUTUBE_API_KEY
+ const videoId = getVideoIdFromUrl(url)
+ const videoUrl = `https://www.googleapis.com/youtube/v3/videos?part=snippet&id=${videoId}&key=${youtubeKey}`
+
+ try {
+ const res = await fetch(videoUrl)
+ const data = await res.json()
+
+ if (data.items && data.items.length > 0) {
+ const videoData = {
+ title: data.items[0].snippet.title,
+ channelTitle: data.items[0].snippet.channelTitle,
+ url: `https://www.youtube.com/watch?v=${videoId}`,
+ thumbnail: data.items[0].snippet.thumbnails.medium.url,
+ }
+ setVideoList((prev: BasicVideoProps[]) => [...prev, videoData])
+ setUrl('')
+ }
+ } catch (error) {
+ console.error('add list error :', error)
+ }
+ }
+
+ const handleDelete = (id: number) => {
+ setVideoList((prev: BasicVideoProps[]) => prev.filter((_, index) => index !== id))
+ }
+
+ return {
+ url,
+ setVideoList,
+ videoList,
+ handleAddList,
+ isValid,
+ setUrl,
+ handleDelete,
+ validType,
+ }
+}
+
+export default usePLItem
diff --git a/src/hooks/usePlaylistData.ts b/src/hooks/usePlaylistData.ts
new file mode 100644
index 00000000..2c0e3bb1
--- /dev/null
+++ b/src/hooks/usePlaylistData.ts
@@ -0,0 +1,26 @@
+import { useQuery } from '@tanstack/react-query'
+import { PlaylistBaseProps } from '@/types/playlistType'
+import getPlaylists from '@/service/playlist/getUserPlaylists'
+import { getUIDFromUserId } from '@/utils/userDataUtils'
+
+const fetchPlaylistData = async (userId?: string) => {
+ if (!userId) return []
+ const userData = await getUIDFromUserId(userId)
+ const playlists = await getPlaylists(userData)
+
+ return playlists.map((playlist) => ({
+ playlistId: playlist.playlistId,
+ title: playlist.title,
+ thumbnail: playlist.thumbnails || 'not valid thumbnail',
+ isPrivate: playlist.isPrivate,
+ createdAt: playlist.createdAt,
+ }))
+}
+
+export const usePlaylistData = (userId?: string) => {
+ return useQuery({
+ queryKey: ['playlists', userId],
+ queryFn: () => fetchPlaylistData(userId),
+ enabled: !!userId,
+ })
+}
diff --git a/src/hooks/usePlaylistDetail.ts b/src/hooks/usePlaylistDetail.ts
new file mode 100644
index 00000000..352bfbde
--- /dev/null
+++ b/src/hooks/usePlaylistDetail.ts
@@ -0,0 +1,22 @@
+import { useEffect, useState } from 'react'
+import { ExtendedVideoProps } from '@/types/playlistType'
+import getUserPlaylistDetails from '@/service/playlist/getUserPlaylistDetails'
+
+export const usePlaylistdetail = (playlistId?: string) => {
+ const [videos, setVideos] = useState([])
+ const [selectedVideo, setSelectedVideo] = useState()
+ useEffect(() => {
+ const fetchPlaylistDetails = async () => {
+ if (playlistId) {
+ const playlistDetails = await getUserPlaylistDetails(playlistId)
+ setVideos(playlistDetails)
+ if (playlistDetails.length > 0) {
+ setSelectedVideo(playlistDetails[0])
+ }
+ }
+ }
+ fetchPlaylistDetails()
+ }, [])
+
+ return { videos, selectedVideo, setSelectedVideo }
+}
diff --git a/src/hooks/useTags.ts b/src/hooks/useTags.ts
new file mode 100644
index 00000000..b7bc8a0f
--- /dev/null
+++ b/src/hooks/useTags.ts
@@ -0,0 +1,65 @@
+import { useState, KeyboardEvent } from 'react'
+import { MESSAGES } from '@/constants/messages'
+
+export const MAX_TAG_COUNT = 6
+export const MAX_TAG_LENGTH = 7
+
+const useTags = () => {
+ const [tags, setTags] = useState([])
+ const [currentTag, setCurrentTag] = useState('')
+ const [isTagValid, setIsTagValid] = useState(true)
+ const [errorMessage, setErrorMessage] = useState('')
+
+ const handleAddTag = (e: KeyboardEvent) => {
+ if (e.key === 'Enter' && currentTag.trim() !== '' && e.nativeEvent.isComposing === false) {
+ e.preventDefault()
+
+ if (currentTag.includes(' ')) {
+ setIsTagValid(false)
+ setErrorMessage(MESSAGES.CREATE_PL.TAG_SPACE_FAIL)
+ setCurrentTag('')
+ return
+ }
+
+ if (tags.length >= MAX_TAG_COUNT) {
+ setIsTagValid(false)
+ setErrorMessage(MESSAGES.CREATE_PL.TAG_COUNT_FAIL)
+ setCurrentTag('')
+ return
+ }
+
+ const saveTag = currentTag.trim().startsWith('#')
+ ? currentTag.trim()
+ : `#${currentTag.trim()}`
+
+ if (saveTag.length > MAX_TAG_LENGTH) {
+ setIsTagValid(false)
+ setErrorMessage(MESSAGES.CREATE_PL.TAG_LENGTH_FAIL)
+ setCurrentTag('')
+ return
+ }
+
+ setIsTagValid(true)
+ setErrorMessage('')
+ setTags((prev: string[]) => [...prev, saveTag])
+ setCurrentTag('')
+ }
+ }
+
+ const handleDeleteTag = (id: number) => {
+ setTags((prev) => prev.filter((_, index) => index !== id))
+ }
+
+ return {
+ tags,
+ currentTag,
+ setCurrentTag,
+ isTagValid,
+ errorMessage,
+ handleAddTag,
+ handleDeleteTag,
+ setTags,
+ }
+}
+
+export default useTags
diff --git a/src/hooks/useUserData.ts b/src/hooks/useUserData.ts
new file mode 100644
index 00000000..418a4deb
--- /dev/null
+++ b/src/hooks/useUserData.ts
@@ -0,0 +1,39 @@
+import { useQuery } from '@tanstack/react-query'
+import { userInfo } from '@/service/profile/profileInfo'
+import NPProfile from '@/assets/np_logo.svg'
+
+interface UserData {
+ userName: string
+ userId: string
+ userImg: string
+ userEmail: string
+ userBio: string
+ followerLength: number
+ followingLength: number
+ playlistLength: number
+}
+
+const fetchUserData = async (userId?: string): Promise => {
+ const data = await userInfo(userId)
+ if (!data) {
+ throw new Error('User not found')
+ }
+ return {
+ userName: data.userName || '사용자',
+ userId: data.userId || 'Unknown',
+ userImg: data.userImg || NPProfile,
+ userEmail: data.userEmail || 'Unknown',
+ userBio: data.userBio || '안녕하세요.',
+ followerLength: data.followerLength || 0,
+ followingLength: data.followingLength || 0,
+ playlistLength: data.playlistLength || 0,
+ }
+}
+
+export const useUserData = (userId?: string) => {
+ return useQuery({
+ queryKey: ['userData', userId],
+ queryFn: () => fetchUserData(userId),
+ enabled: !!userId,
+ })
+}
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 6119ad9a..00000000
--- a/src/index.css
+++ /dev/null
@@ -1,68 +0,0 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/src/layout/Root.tsx b/src/layout/Root.tsx
new file mode 100644
index 00000000..c703d18f
--- /dev/null
+++ b/src/layout/Root.tsx
@@ -0,0 +1,68 @@
+import { Outlet, useLocation } from 'react-router-dom'
+import Header from '@/components/layout/header/Header'
+import Navbar from '@/components/layout/Navbar'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { PATH } from '@/constants/path'
+import BackHeader from '@/components/layout/header/BackHeader'
+import { useAuth } from '@/hooks/auth/useAuth'
+import Loading from '@/components/common/Loading'
+
+const RootLayout = () => {
+ const { isAuthenticated, isLoading } = useAuth()
+ const location = useLocation()
+ const showBackButton = location.pathname === PATH.SIGNUP || location.pathname === PATH.EDITPW
+
+ if (isLoading) {
+ return
+ }
+
+ return (
+
+
+
+ {isAuthenticated ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>
+ {showBackButton && }
+
+ >
+ )}
+
+
+ )
+}
+
+export default RootLayout
+
+const Container = styled.div`
+ position: relative;
+ width: 100%;
+ height: 100vh;
+
+ .background-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: ${colors.lightestGray};
+ z-index: 1;
+ }
+
+ .root {
+ position: relative;
+ z-index: 2;
+ justify-content: space-between;
+ max-width: 430px;
+ min-height: 100%;
+ margin: 0 auto;
+ padding-bottom: 52px;
+ background-color: ${colors.white};
+ }
+`
diff --git a/src/main.tsx b/src/main.tsx
index ee6e5e6f..092ea0a8 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,7 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
+import App from '@/App'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx
new file mode 100644
index 00000000..49026139
--- /dev/null
+++ b/src/pages/ChatPage.tsx
@@ -0,0 +1,19 @@
+import styled from '@emotion/styled'
+
+const ChatPage = () => {
+ return (
+
+ 서비스 준비 중 입니다.
+
+ )
+}
+
+export default ChatPage
+
+const Container = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: calc(90vh - 100px);
+ min-height: 300px;
+`
diff --git a/src/pages/CreatePlaylistPage.tsx b/src/pages/CreatePlaylistPage.tsx
new file mode 100644
index 00000000..ed6b58b2
--- /dev/null
+++ b/src/pages/CreatePlaylistPage.tsx
@@ -0,0 +1,268 @@
+import React, { useState } from 'react'
+import styled from '@emotion/styled'
+import { colors } from '@/styles/colors'
+import { fontSize } from '@/styles/font'
+import Button from '@/components/common/Button/Button'
+import PLlogo from '@/assets/createlist_logo.svg'
+import LineInput from '@/components/common/Input/LineInput'
+import { MESSAGES } from '@/constants/messages'
+import useTags from '@/hooks/useTags'
+import usePLItem from '@/hooks/usePLItem'
+import MusicItem from '@/components/playlist/MusicItem'
+import createNewPlaylist from '@/service/playlist/createNewPlaylist'
+import { useNavigate } from 'react-router-dom'
+import { getLoggedInUserUID, getUserIdFromUID } from '@/utils/userDataUtils'
+import TagList from '@/components/playlist/TagList'
+
+const CreatePlaylistPage = () => {
+ const navigate = useNavigate()
+ const [title, setTitle] = useState('')
+ const [isPrivate, setIsPrivate] = useState(false)
+ const uid = getLoggedInUserUID()
+ const {
+ errorMessage,
+ tags,
+ currentTag,
+ setCurrentTag,
+ isTagValid,
+ handleAddTag,
+ handleDeleteTag,
+ setTags,
+ } = useTags()
+ const { url, setVideoList, videoList, handleAddList, isValid, setUrl, handleDelete, validType } =
+ usePLItem()
+
+ const handleUploadMusic = async (e: React.FormEvent) => {
+ e.preventDefault()
+ try {
+ await createNewPlaylist(title, tags, videoList, isPrivate)
+ setTitle('')
+ setTags([])
+ setUrl('')
+ setVideoList([])
+ setCurrentTag('')
+ setIsPrivate(false)
+
+ if (uid) {
+ const id = await getUserIdFromUID(uid)
+
+ if (id) {
+ navigate(`/profile/${id}`)
+ }
+ }
+ } catch (error) {
+ console.error('Error adding playlist: ', error)
+ }
+ }
+
+ const handlePrivate = () => {
+ setIsPrivate(!isPrivate)
+ }
+
+ const isUploadDisabled = title.trim() === '' || tags.length === 0 || videoList.length === 0
+
+ return (
+
+
+
+ )
+}
+
+export default CreatePlaylistPage
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ height: 80vh;
+
+ .pl-upload {
+ position: fixed;
+ bottom: 51px;
+ width: 100%;
+ max-width: 430px;
+ button {
+ border-radius: 0px;
+ }
+ }
+
+ .list-music,
+ .input-youtube,
+ .section-upload,
+ .text-warning {
+ margin: 0 20px;
+ }
+
+ .section-input {
+ margin: 10px 20px 0;
+ }
+
+ .section-upload {
+ align-items: center;
+ }
+
+ .input-link {
+ width: 100%;
+ margin-bottom: 5px;
+ padding: 16px 18px;
+ border: 1px solid ${colors.lightGray};
+ border-radius: 5px;
+ }
+
+ .input-title,
+ .input-tag,
+ .btn-upload,
+ .btn_add {
+ width: 100%;
+ padding: 16px 18px;
+ border: 1px solid ${colors.lightGray};
+ border-radius: 5px;
+ }
+
+ .list-music {
+ padding: 20px 0;
+ display: flex;
+ height: 210px;
+ max-height: 380px;
+ flex-direction: column;
+ gap: 15px;
+ overflow-y: auto;
+ @media (min-width: 400px) {
+ height: 420px;
+ }
+
+ .pl-notice {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ color: ${colors.gray};
+ font-size: ${fontSize.md};
+ text-align: center;
+ }
+ .pl-logo {
+ width: 73px;
+ height: 73px;
+ margin: 20px 0 20px 0;
+ }
+ }
+
+ .input-youtube {
+ display: flex;
+ gap: 10px;
+ }
+
+ .btn-add {
+ right: 10px;
+ width: 100px;
+ top: 10px;
+ border: 1px solid ${colors.lightGray};
+ color: ${colors.white};
+ background-color: ${colors.primaryPurple};
+ border-radius: 15px;
+ margin-bottom: 5px;
+ cursor: pointer;
+ }
+
+ .btn-delete {
+ width: 38px;
+ height: 37px;
+ border: 1px solid ${colors.lightestGray};
+ border-radius: 50px;
+ padding: 5px;
+ cursor: pointer;
+ }
+
+ .text-warning {
+ color: red;
+ font-size: ${fontSize.sm};
+ }
+
+ .check-private {
+ display: flex;
+ align-items: center;
+ margin-bottom: 5px;
+ margin-left: 10px;
+
+ input {
+ appearance: none;
+ margin-right: 10px;
+ width: 20px;
+ height: 20px;
+ border: 2px solid ${colors.lightGray};
+ border-radius: 4px;
+ cursor: pointer;
+ position: relative;
+ }
+
+ input:checked {
+ background-color: ${colors.primaryPurple};
+ border-color: ${colors.black};
+ }
+ }
+`
diff --git a/src/pages/FollowPage.tsx b/src/pages/FollowPage.tsx
new file mode 100644
index 00000000..f98964bf
--- /dev/null
+++ b/src/pages/FollowPage.tsx
@@ -0,0 +1,5 @@
+const FollowPage = () => {
+ return Follow
+}
+
+export default FollowPage
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 00000000..6891f0b4
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,154 @@
+import { useState, useEffect, useCallback } from 'react'
+import { useInfiniteQuery } from '@tanstack/react-query'
+import styled from '@emotion/styled'
+import getPaginatedFollowingUsersPlaylists from '@/service/playlist/getPaginatedFollowingUsersPlaylists'
+import UserInfo from '@/components/playlist/UserInfo'
+import PlaylistThumbnailFeed from '@/components/playlist/PlaylistThumbnailFeed'
+import LikeButton from '@/components/common/Button/LikeButton'
+import { UserRoundPlus } from 'lucide-react'
+import { colors } from '@/styles/colors'
+import { fontSize, fontWeight } from '@/styles/font'
+import { useNavigate } from 'react-router-dom'
+import { FollowedPlaylist, FollowedUserPlaylists } from '@/types/playlistType'
+import Loading from '@/components/common/Loading'
+
+const HomePage = () => {
+ const navigate = useNavigate()
+ const [allPlaylists, setAllPlaylists] = useState([])
+ const limit = 3
+
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteQuery({
+ queryKey: ['followingPlaylists', limit],
+ queryFn: ({ pageParam = 0 }) =>
+ getPaginatedFollowingUsersPlaylists({ startIndex: pageParam, limit }),
+ getNextPageParam: (lastPage, allPages) => {
+ if (lastPage.length === limit) {
+ return allPages.length * limit
+ } else {
+ return undefined
+ }
+ },
+ initialPageParam: 0,
+ })
+
+ const handleScroll = useCallback(() => {
+ if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 100) {
+ if (hasNextPage && !isFetchingNextPage) {
+ fetchNextPage()
+ }
+ }
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage])
+
+ const truncateText = (text: string | undefined, maxLength: number) => {
+ if (!text) return ''
+ return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text
+ }
+
+ useEffect(() => {
+ window.addEventListener('scroll', handleScroll)
+ return () => {
+ window.removeEventListener('scroll', handleScroll)
+ }
+ }, [handleScroll])
+
+ useEffect(() => {
+ if (data?.pages) {
+ const mergedPlaylists = data.pages
+ .flatMap((page) =>
+ page.flatMap((user: FollowedUserPlaylists) =>
+ user.playlists.map((playlist: FollowedPlaylist) => ({
+ ...playlist,
+ }))
+ )
+ )
+ .sort(
+ (a: FollowedPlaylist, b: FollowedPlaylist) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ )
+
+ setAllPlaylists(mergedPlaylists)
+ }
+ }, [data])
+
+ return (
+
+ {isLoading ? (
+
+ ) : allPlaylists.length > 0 ? (
+
+ {allPlaylists.map(
+ ({ playlistId, authorId, authorName, authorImg, title, createdAt, thumbnails }) => (
+ -
+ navigate(`/profile/${authorId}`)}
+ />
+
navigate(`/playlist/${playlistId}`)}
+ className="playlist-detail-container"
+ >
+
{truncateText(title, 60)}
+
+
+
+
+ )
+ )}
+
+ ) : (
+
+
+
아직 팔로우한 사용자가 없습니다.
+
+ 마음에 드는 사용자를 팔로우하고
플레이리스트를 확인해보세요!
+
+
+ )}
+ {isFetchingNextPage && Loading more...
}
+
+ )
+}
+
+export default HomePage
+
+const Container = styled.div`
+ .playlist-container {
+ padding: 20px;
+ border-bottom: 6px solid ${colors.lightestGray};
+ cursor: pointer;
+ }
+
+ .user-info-container {
+ margin-bottom: 12px;
+ }
+
+ .playlist-title {
+ margin-bottom: 4px;
+ }
+
+ .playlist-detail-container {
+ margin-bottom: 8px;
+ }
+
+ .no-following-container {
+ margin-top: 100px;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ text-align: center;
+ color: ${colors.gray};
+
+ .no-following-title {
+ margin-top: 20px;
+ font-size: ${fontSize.lg};
+ font-weight: ${fontWeight.bold};
+ }
+
+ .no-following-text {
+ margin-top: 10px;
+ }
+ }
+`
diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx
new file mode 100644
index 00000000..3efca98e
--- /dev/null
+++ b/src/pages/LoginPage.tsx
@@ -0,0 +1,173 @@
+import styled from '@emotion/styled'
+import React, { useEffect, useState } from 'react'
+import { PATH } from '@/constants/path'
+import logo from '@/assets/myidoru_logo.svg'
+import { fontSize } from '@/styles/font'
+import Button from '@/components/common/Button/Button'
+import ButtonLink from '@/components/common/Button/ButtonLink'
+import ButtonImage from '@/components/common/Button/ButtonImage'
+import Input from '@/components/common/Input/Input'
+import { colors } from '@/styles/colors'
+import { MESSAGES } from '@/constants/messages'
+import login from '@/service/auth/login'
+import { useNavigate } from 'react-router-dom'
+import googleLogin from '@/service/auth/googleLogin'
+import { useAuth } from '@/hooks/auth/useAuth'
+
+const LoginPage = () => {
+ const { isAuthenticated } = useAuth()
+ const [loginInfo, setLoginInfo] = useState({
+ email: '',
+ password: '',
+ })
+ const [isLoginError, setIsLoginError] = useState(false)
+ const navigate = useNavigate()
+ //TODO: 이메일, 비밀번호 유효성 검사하기
+ const isValid = loginInfo.email && loginInfo.password
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target
+ setLoginInfo({
+ ...loginInfo,
+ [name]: value,
+ })
+ }
+
+ const handleEmailLogin = async (e: React.FormEvent) => {
+ e.preventDefault()
+ const { email, password } = loginInfo
+ const isLoginSuccess = await login(email, password)
+
+ if (isLoginSuccess) {
+ navigate('/')
+ } else {
+ setIsLoginError(true)
+ setLoginInfo({
+ email: '',
+ password: '',
+ })
+ }
+ }
+
+ const handleGoogleLogin = async () => {
+ const isLoginSuccess = await googleLogin()
+ if (isLoginSuccess) {
+ navigate('/')
+ }
+ }
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ navigate('/')
+ }
+ }, [isAuthenticated])
+
+ return (
+
+
+

+
+
+
+
+
+ 비밀번호를 잊으셨나요?
+
+
+
+
+ 또는
+
+
+
+
+
+ 이메일로 회원가입
+
+
+
+ )
+}
+
+export default LoginPage
+
+const Container = styled.div`
+ padding: 62px 20px;
+
+ .logo-container {
+ margin-bottom: 40px;
+ display: flex;
+ justify-content: center;
+ }
+
+ .login-container {
+ margin-bottom: 22px;
+ .form-login {
+ margin-bottom: 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ width: 100%;
+ }
+
+ .login-notice {
+ color: ${colors.red};
+ font-size: ${fontSize.sm};
+ }
+ }
+
+ .divider {
+ margin-bottom: 22px;
+ display: flex;
+ align-items: center;
+ text-align: center;
+ width: 100%;
+
+ &::before,
+ &::after {
+ content: '';
+ flex: 1;
+ border-bottom: 1px solid ${colors.lightGray};
+ }
+
+ &::before {
+ margin-right: 16px;
+ }
+
+ &::after {
+ margin-left: 16px;
+ }
+
+ & span {
+ color: ${colors.gray};
+ font-size: ${fontSize.sm};
+ white-space: nowrap;
+ }
+ }
+
+ .auth-container {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+`
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
new file mode 100644
index 00000000..7f61f62d
--- /dev/null
+++ b/src/pages/NotFoundPage.tsx
@@ -0,0 +1,75 @@
+import ButtonLink from '@/components/common/Button/ButtonLink'
+import { colors } from '@/styles/colors'
+import { fontSize, fontWeight } from '@/styles/font'
+import styled from '@emotion/styled'
+
+const NotFoundPage = () => {
+ return (
+
+
+
404
+
원하시는 페이지를 찾을 수 없습니다.
+
+ 원하시는 페이지를 찾을 수 없습니다.
+
+ 찾으려는 페이지의 주소가 잘못 입력되었거나,
+
+ 주소의 변경 혹은 삭제로 인해 사용할 수 없습니다.
+
+ 입력하신 페이지의 주소가 정확한지
+
+ 다시 한번 확인해 주세요.
+
+
+ 홈으로 가기
+
+
+
+ )
+}
+
+export default NotFoundPage
+
+const NotFoundPageContainer = styled.div`
+ background-color: ${colors.lightestGray};
+
+ .content-container {
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ max-width: 430px;
+ height: 100vh;
+ background-color: ${colors.white};
+ padding: 0 20px;
+ text-align: center;
+ }
+
+ .title {
+ font-size: 100px;
+ font-weight: ${fontWeight.extraBold};
+ color: ${colors.primaryPurple};
+ }
+
+ .sub-title {
+ font-size: ${fontSize.xl};
+ margin: 0;
+ color: ${colors.black};
+ margin-bottom: 16px;
+
+ @media (min-width: 400px) {
+ font-size: ${fontSize.xxl};
+ }
+ }
+
+ .description {
+ margin: 0;
+ margin-bottom: 24px;
+ color: ${colors.gray};
+ line-height: 1.6;
+ @media (min-width: 400px) {
+ font-size: ${fontSize.lg};
+ }
+ }
+`
diff --git a/src/pages/PlaylistPage.tsx b/src/pages/PlaylistPage.tsx
new file mode 100644
index 00000000..035b4a14
--- /dev/null
+++ b/src/pages/PlaylistPage.tsx
@@ -0,0 +1,244 @@
+import styled from '@emotion/styled'
+import { useParams, Link } from 'react-router-dom'
+import { useState } from 'react'
+import { colors } from '@/styles/colors'
+import Button from '@/components/common/Button/Button'
+import { usePlaylistdetail } from '@/hooks/usePlaylistDetail'
+import Comments from '@/components/comments/Comments'
+import NPProfile from '@/assets/np_logo.svg'
+import Avatar from '@/components/common/Avatar'
+import LikeButton from '@/components/common/Button/LikeButton'
+
+const PlaylistPage = () => {
+ const { playlistId } = useParams<{ playlistId: string }>()
+ const [tab, setTab] = useState<'videos' | 'comments'>('videos')
+ const { videos, selectedVideo, setSelectedVideo } = usePlaylistdetail(playlistId)
+
+ const renderVideos = () => (
+
+ {videos.map((video, idx) => (
+ {
+ setSelectedVideo(videos[idx])
+ }}
+ >
+
[1]?.split('&')[0]}/hqdefault.jpg`})
+
{video.title}
+
+ ))}
+
+ )
+
+ const renderComments = () => (
+
+
+
+ )
+
+ return (
+
+
+ {selectedVideo ? (
+
+ ) : (
+
Select video
+ )}
+
+
{selectedVideo?.playlistTitle}
+
+
+
+
+
+
+
+
{selectedVideo?.author}
+
+
{selectedVideo?.uploadDate}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tab === 'videos' ? renderVideos() : renderComments()}
+
+ )
+}
+
+export default PlaylistPage
+
+export const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+
+ .section-btn {
+ display: flex;
+ width: 150px;
+ gap: 10px;
+ padding: 0 20px;
+ }
+
+ .divider {
+ background-color: ${colors.lightestGray};
+ width: 100%;
+ height: 6px;
+ }
+
+ .video-player {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0 20px 10px;
+ margin-top: 10px;
+
+ iframe {
+ max-width: 100%;
+ border-radius: 12px;
+ margin-bottom: 10px;
+ }
+
+ .info-video {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .playlist-title {
+ font-weight: bold;
+ font-size: 18px;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ word-break: break-word;
+ -webkit-box-orient: vertical;
+ }
+
+ .info-details {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .info-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+
+ a {
+ text-decoration: none;
+ display: flex;
+ }
+ }
+
+ .info-author-date {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .info-author {
+ font-size: 14px;
+ font-weight: bold;
+ }
+
+ .info-date {
+ font-size: 12px;
+ color: #666;
+ }
+
+ .info-likes {
+ font-size: 14px;
+ color: #888;
+ }
+ }
+ }
+ }
+`
+
+const VideoList = styled.div`
+ width: 100%;
+ max-width: 600px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0 20px;
+
+ .video-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ cursor: pointer;
+ background-color: #f9f9f9;
+
+ &.active {
+ background-color: ${colors.lightPurPle};
+ }
+
+ &:hover {
+ background-color: ${colors.lightPurPle};
+ }
+
+ .video-thumbnail {
+ width: 130px;
+ height: 70px;
+ object-fit: cover;
+ }
+
+ .video-title {
+ flex: 1;
+ font-size: 14px;
+ line-height: 1.2;
+ }
+ }
+`
+
+const CommentList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0 20px;
+
+ .comment-item {
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-radius: 8px;
+ background-color: #f9f9f9;
+ }
+`
diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx
new file mode 100644
index 00000000..49bf681f
--- /dev/null
+++ b/src/pages/ProfilePage.tsx
@@ -0,0 +1,199 @@
+import styled from '@emotion/styled'
+import { useState } from 'react'
+import { useParams } from 'react-router-dom'
+import { fontSize, fontWeight } from '@/styles/font'
+import Button from '@/components/common/Button/Button'
+import { colors } from '@/styles/colors'
+import NPProfile from '@/assets/np_logo.svg'
+import { useUserData } from '@/hooks/useUserData'
+import { usePlaylistData } from '@/hooks/usePlaylistData'
+import type { filterPlaylist, PlaylistBaseProps } from '@/types/playlistType'
+import MusicItem from '@/components/playlist/MusicItem'
+import { getLoggedInUserUID } from '@/utils/userDataUtils'
+import { useFollowButton } from '@/hooks/useFollowStatus'
+import { useIsMyProfile } from '@/hooks/useIsMyProfile'
+import Avatar from '@/components/common/Avatar'
+
+const filterBtns = [
+ {
+ label: '전체',
+ value: 'all' as filterPlaylist,
+ },
+ {
+ label: '공개',
+ value: 'public' as filterPlaylist,
+ },
+ {
+ label: '비공개',
+ value: 'private' as filterPlaylist,
+ },
+]
+
+const ProfilePage = () => {
+ const { userId } = useParams<{ userId?: string }>()
+ const currentUser = getLoggedInUserUID()
+ const { data: userData } = useUserData(userId)
+ const { data: playlistData = [] } = usePlaylistData(userId)
+ const { data: isMyProfile } = useIsMyProfile(userId)
+ const [filter, setFilter] = useState('all')
+ const [isOpen, setIsOpen] = useState(false)
+ const { isFollowing, toggleFollow, followerCount } = useFollowButton(
+ userData?.userId || '',
+ currentUser || ''
+ )
+
+ const handleFilterChange = (newFilter: filterPlaylist) => {
+ setFilter(newFilter)
+ }
+
+ const filteredMap: Record boolean> = {
+ all: () => true as boolean,
+ public: (playlistData) => !playlistData.isPrivate,
+ private: (playlistData) => !!playlistData.isPrivate,
+ }
+
+ const filteredLists = playlistData
+ .filter((playlistData: PlaylistBaseProps) => {
+ return !isMyProfile && playlistData.isPrivate ? false : filteredMap[filter](playlistData)
+ })
+ .sort(
+ (a: PlaylistBaseProps, b: PlaylistBaseProps) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ )
+
+ const displayedLists = isOpen ? filteredLists : filteredLists.slice(0, 4)
+
+ const handleToggleOpen = () => {
+ setIsOpen((prev) => !prev)
+ }
+
+ const infoItems = [
+ {
+ label: '플리',
+ value: userData?.playlistLength,
+ },
+ {
+ label: '팔로워',
+ value: followerCount,
+ },
+ {
+ label: '팔로잉',
+ value: userData?.followingLength,
+ },
+ ]
+
+ return (
+
+ {!isMyProfile && userData?.userId}
+
+
+
+
+ {infoItems.map((item, index) => (
+
+
{item.value}
+ {item.label}
+
+ ))}
+
+
+
{userData?.userBio}
+ {!isMyProfile && (
+
+ )}
+
+
+
+
플레이리스트
+ {isMyProfile && (
+
+ {filterBtns.map((btn) => (
+
+ ))}
+
+ )}
+
+
+ {filteredLists.length > 4 && (
+
+ )}
+
+ )
+}
+
+export default ProfilePage
+
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ .section-head {
+ display: flex;
+ justify-content: space-between;
+ padding: 0 20px;
+ font-size: ${fontSize.lg};
+ font-weight: ${fontWeight.bold};
+ }
+
+ .section-userinfo {
+ padding: 10px 20px 20px 20px;
+ }
+
+ .user-bio,
+ .title-playlist {
+ font-size: ${fontSize.md};
+ font-weight: ${fontWeight.semiBold};
+ }
+ .section-info,
+ .section-playlist,
+ .section-btn {
+ font-size: ${fontSize.md};
+ font-weight: ${fontWeight.bold};
+ }
+
+ .profile {
+ display: flex;
+ justify-content: space-between;
+ text-align: center;
+ height: 52px;
+ }
+
+ .section-info {
+ gap: 50px;
+ display: flex;
+ padding: 0 10px;
+ }
+
+ .user-bio {
+ margin: 12px 0;
+ width: 100%;
+ }
+
+ .divider {
+ background-color: ${colors.lightestGray};
+ width: 100%;
+ height: 6px;
+ }
+
+ .section-btn {
+ display: flex;
+ gap: 5px;
+ width: 230px;
+ margin: 20px 0;
+ }
+
+ .section-playlist {
+ padding: 20px 20px 0;
+ }
+`
diff --git a/src/pages/ResetPwPage.tsx b/src/pages/ResetPwPage.tsx
new file mode 100644
index 00000000..827fe400
--- /dev/null
+++ b/src/pages/ResetPwPage.tsx
@@ -0,0 +1,89 @@
+import styled from '@emotion/styled'
+import { sendPasswordResetEmail } from 'firebase/auth'
+import { auth } from '@/firebase/firebaseConfig'
+import { useState } from 'react'
+import { fontSize, fontWeight } from '@/styles/font'
+import { colors } from '@/styles/colors'
+import Input from '@/components/common/Input/Input'
+import Button from '@/components/common/Button/Button'
+import { MESSAGES } from '@/constants/messages'
+
+const ResetPwPage = () => {
+ const [email, setEmail] = useState('')
+ const [message, setMessage] = useState('')
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ const isValid = emailRegex.test(email)
+
+ const handleResetPassword = async (e: React.FormEvent) => {
+ e.preventDefault()
+ try {
+ if (isValid) {
+ await sendPasswordResetEmail(auth, email)
+ setMessage(MESSAGES.RESET_PASSWORD.SUCCESS)
+ }
+ } catch (error) {
+ setMessage(MESSAGES.RESET_PASSWORD.FAIL)
+ setEmail('')
+ }
+ }
+ return (
+
+ 비밀번호를 잊으셨나요?
+
+ 가입하신 이메일을 입력하시면,
+
비밀번호 재설정 링크를 보내드립니다.
+
+
+
+ )
+}
+
+export default ResetPwPage
+
+const Container = styled.div`
+ padding: 62px 20px;
+
+ .editpw-title {
+ margin-bottom: 22px;
+ font-size: ${fontSize.xl};
+ }
+
+ .editpw-text {
+ margin-bottom: 22px;
+ font-weight: ${fontWeight.medium};
+ }
+
+ .form-editpw {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+
+ .message {
+ font-size: ${fontSize.sm};
+ }
+
+ .success {
+ color: ${colors.primaryPurple};
+ }
+
+ .failed {
+ color: ${colors.red};
+ }
+ }
+`
diff --git a/src/pages/SearchPage.tsx b/src/pages/SearchPage.tsx
new file mode 100644
index 00000000..6cb27a10
--- /dev/null
+++ b/src/pages/SearchPage.tsx
@@ -0,0 +1,84 @@
+import { useState } from 'react'
+import { Playlist, SearchTag } from '@/service/search/searchTag'
+import styled from '@emotion/styled'
+import Input from '@/components/common/Input/Input'
+import { colors } from '@/styles/colors'
+import RecommendSearch from '@/components/search/RecommendSearch'
+import { Keywords } from '@/components/search/RecommendKeyword'
+import SearchFail from '@/components/search/SearchFail'
+import SearchSuccess from '@/components/search/SearchSuccess'
+
+const SearchPage = () => {
+ const [searchTag, setSearchTag] = useState('')
+ const [playlists, setPlaylists] = useState([])
+ const [previousSearchTag, setPreviousSearchTag] = useState('')
+ const [hasSearched, setHasSearched] = useState(false)
+
+ const handleSearch = async () => {
+ if (!searchTag.trim()) {
+ setHasSearched(false)
+ return
+ }
+
+ const formattedSearchTag = !searchTag.startsWith('#') ? '#' + searchTag : searchTag
+ const results = await SearchTag(formattedSearchTag)
+ setPlaylists(results)
+ setHasSearched(true)
+ setPreviousSearchTag(searchTag)
+ setSearchTag('')
+ }
+
+ const recommendedKeywords = Keywords()
+
+ return (
+
+
+ setSearchTag(e.target.value)}
+ placeholder="플레이리스트 관련 태그 검색"
+ />
+
+
+
+ {!hasSearched ? (
+
+ ) : playlists.length === 0 ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+export default SearchPage
+
+const Container = styled.div`
+ padding: 30px 20px 0 20px;
+ margin-bottom: 16px;
+ .search-tag {
+ display: flex;
+ gap: 10px;
+ }
+ .search-input {
+ width: 100%;
+ margin-bottom: 5px;
+ padding: 16px 18px;
+ border: 1px solid ${colors.lightGray};
+ border-radius: 5px;
+ }
+ .search-btn {
+ width: 100px;
+ border: 1px solid ${colors.lightGray};
+ color: ${colors.white};
+ background-color: ${colors.primaryPurple};
+ border-radius: 15px;
+ margin-bottom: 5px;
+ cursor: pointer;
+ }
+`
diff --git a/src/pages/SignUpPage.tsx b/src/pages/SignUpPage.tsx
new file mode 100644
index 00000000..c0717b8c
--- /dev/null
+++ b/src/pages/SignUpPage.tsx
@@ -0,0 +1,29 @@
+import { useState } from 'react'
+import Form from '@/components/Form'
+import { useNavigate } from 'react-router-dom'
+import { app } from '@/firebase/firebaseConfig'
+import { createUserWithEmailAndPassword, getAuth } from 'firebase/auth'
+import { FormInputs } from '@/types/formType'
+
+const SignUp = () => {
+ const navigate = useNavigate()
+ const [firebaseError, setFirebaseError] = useState('')
+
+ const auth = getAuth(app)
+ const handleSignupAndLogin = ({ email, password }: FormInputs) => {
+ createUserWithEmailAndPassword(auth, email, password)
+ .then(() => {
+ navigate('/')
+ })
+ .catch((error) => {
+ console.error('Firebase error:', error.code, error.message)
+ setFirebaseError(`${error.code}: ${error.message}`)
+ })
+ }
+
+ return (
+
+ )
+}
+
+export default SignUp
diff --git a/src/routes/PrivateRoute.tsx b/src/routes/PrivateRoute.tsx
new file mode 100644
index 00000000..dcabbad6
--- /dev/null
+++ b/src/routes/PrivateRoute.tsx
@@ -0,0 +1,10 @@
+import { Navigate, Outlet } from 'react-router-dom'
+import { PATH } from '@/constants/path'
+import { useAuth } from '@/hooks/auth/useAuth'
+
+const PrivateRoute = () => {
+ const { isAuthenticated } = useAuth()
+ return isAuthenticated ? :
+}
+
+export default PrivateRoute
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 00000000..43329a58
--- /dev/null
+++ b/src/routes/index.tsx
@@ -0,0 +1,51 @@
+import { PATH } from '@/constants/path'
+import { createBrowserRouter } from 'react-router-dom'
+import CreatePlaylistPage from '@/pages/CreatePlaylistPage'
+import FollowPage from '@/pages/FollowPage'
+import LoginPage from '@/pages/LoginPage'
+import NotFoundPage from '@/pages/NotFoundPage'
+import PlaylistPage from '@/pages/PlaylistPage'
+import ProfilePage from '@/pages/ProfilePage'
+import SearchPage from '@/pages/SearchPage'
+import RootLayout from '@/layout/Root'
+import ChatPage from '@/pages/ChatPage'
+import HomePage from '@/pages/HomePage'
+import ResetPwPage from '@/pages/ResetPwPage'
+import SignUpPage from '@/pages/SignUpPage'
+import PrivateRoute from '@/routes/PrivateRoute'
+
+const router = createBrowserRouter([
+ {
+ path: PATH.HOME,
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: PATH.LOGIN,
+ element: ,
+ },
+ {
+ path: PATH.SIGNUP,
+ element: ,
+ },
+ { path: PATH.EDITPW, element: },
+ {
+ element: ,
+ children: [
+ {
+ index: true,
+ element: ,
+ },
+ { path: PATH.PLAYLIST, element: },
+ { path: PATH.USER_PROFILE, element: },
+ { path: PATH.CREATEPLAYLIST, element: },
+ { path: PATH.FOLLOW, element: },
+ { path: PATH.CHAT, element: },
+ { path: PATH.SEARCH, element: },
+ ],
+ },
+ ],
+ },
+])
+
+export default router
diff --git a/src/service/auth/checkAuth.ts b/src/service/auth/checkAuth.ts
new file mode 100644
index 00000000..c8a0f74f
--- /dev/null
+++ b/src/service/auth/checkAuth.ts
@@ -0,0 +1,10 @@
+import { FIREBASE_SESSION_KEY } from '@/constants/firebaseKeys'
+
+const checkAuth = (): boolean => {
+ const sessionKey = FIREBASE_SESSION_KEY
+ const isLogin = sessionStorage.getItem(sessionKey)
+
+ return !!isLogin
+}
+
+export default checkAuth
diff --git a/src/service/auth/googleLogin.ts b/src/service/auth/googleLogin.ts
new file mode 100644
index 00000000..2328448f
--- /dev/null
+++ b/src/service/auth/googleLogin.ts
@@ -0,0 +1,40 @@
+import { FIREBASE_SESSION_KEY } from '@/constants/firebaseKeys'
+import { auth, db } from '@/firebase/firebaseConfig'
+import { signInWithPopup, GoogleAuthProvider } from 'firebase/auth'
+import { doc, setDoc, getDoc } from 'firebase/firestore'
+
+const googleLogin = async () => {
+ try {
+ const provider = new GoogleAuthProvider()
+ const result = await signInWithPopup(auth, provider)
+
+ const user = result.user
+ const userId = user.email?.split('@')[0]
+ const userName = user.displayName
+ const userEmail = user.email
+
+ const userProfileRef = doc(db, `USERS/${user.uid}`)
+
+ const docSnapshot = await getDoc(userProfileRef)
+ if (!docSnapshot.exists()) {
+ await setDoc(userProfileRef, {
+ id: userId,
+ name: userName,
+ email: userEmail,
+ bio: '',
+ followers: 0,
+ following: 0,
+ img: '',
+ createdAt: new Date(),
+ })
+ }
+
+ sessionStorage.setItem(FIREBASE_SESSION_KEY, JSON.stringify(user))
+
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+export default googleLogin
diff --git a/src/service/auth/login.ts b/src/service/auth/login.ts
new file mode 100644
index 00000000..937c8992
--- /dev/null
+++ b/src/service/auth/login.ts
@@ -0,0 +1,19 @@
+import { auth } from '@/firebase/firebaseConfig'
+import {
+ browserSessionPersistence,
+ setPersistence,
+ signInWithEmailAndPassword,
+} from 'firebase/auth'
+
+const login = async (email: string, password: string) => {
+ try {
+ await setPersistence(auth, browserSessionPersistence)
+ await signInWithEmailAndPassword(auth, email, password)
+
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+export default login
diff --git a/src/service/auth/logout.ts b/src/service/auth/logout.ts
new file mode 100644
index 00000000..b4455130
--- /dev/null
+++ b/src/service/auth/logout.ts
@@ -0,0 +1,19 @@
+import { FIREBASE_SESSION_KEY } from '@/constants/firebaseKeys'
+import { auth } from '@/firebase/firebaseConfig'
+import { signOut } from 'firebase/auth'
+
+const logout = async () => {
+ try {
+ await signOut(auth)
+ const sessionKey = FIREBASE_SESSION_KEY
+
+ if (sessionStorage.getItem(sessionKey)) {
+ sessionStorage.removeItem(sessionKey)
+ }
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
+export default logout
diff --git a/src/service/comment/createComment.ts b/src/service/comment/createComment.ts
new file mode 100644
index 00000000..a8f62fe9
--- /dev/null
+++ b/src/service/comment/createComment.ts
@@ -0,0 +1,29 @@
+import { db, auth } from '@/firebase/firebaseConfig'
+import { doc, setDoc, serverTimestamp, collection } from 'firebase/firestore'
+
+// 댓글을 생성하는 함수
+const createComment = async (playlistId: string, comment: string) => {
+ try {
+ const { currentUser } = auth
+ if (!currentUser) {
+ throw new Error('User not authenticated')
+ }
+
+ const userId = currentUser.uid
+
+ const userRef = doc(db, `USERS/${userId}`)
+
+ const commentRef = doc(collection(db, `PLAYLISTS/${playlistId}/COMMENTS`))
+
+ await setDoc(commentRef, {
+ comment: comment,
+ createdAt: serverTimestamp(),
+ userRef: userRef,
+ })
+ } catch (error) {
+ console.error('Error creating comment:', error)
+ throw new Error('Failed to create comment')
+ }
+}
+
+export default createComment
diff --git a/src/service/comment/deleteComment.ts b/src/service/comment/deleteComment.ts
new file mode 100644
index 00000000..88908b31
--- /dev/null
+++ b/src/service/comment/deleteComment.ts
@@ -0,0 +1,16 @@
+import { db } from '@/firebase/firebaseConfig'
+import { doc, deleteDoc } from 'firebase/firestore'
+
+// 특정 플레이리스트의 댓글을 삭제하는 함수
+const deleteComment = async (playlistId: string, commentId: string) => {
+ try {
+ const commentRef = doc(db, `PLAYLISTS/${playlistId}/COMMENTS`, commentId)
+
+ await deleteDoc(commentRef)
+ } catch (error) {
+ console.error('Error deleting comment:', error)
+ throw new Error('Failed to delete comment')
+ }
+}
+
+export default deleteComment
diff --git a/src/service/comment/getAllCommentsByPlaylistId.ts b/src/service/comment/getAllCommentsByPlaylistId.ts
new file mode 100644
index 00000000..c852271e
--- /dev/null
+++ b/src/service/comment/getAllCommentsByPlaylistId.ts
@@ -0,0 +1,62 @@
+import { db } from '@/firebase/firebaseConfig'
+import formatDate from '@/utils/formatDate'
+import { collection, query, getDocs, getDoc, orderBy } from 'firebase/firestore'
+
+interface User {
+ id: string
+ img: string
+ name: string
+}
+
+// 주어진 플레이리스트 ID에 대한 댓글 목록을 가져오는 함수
+const getAllCommentsByPlaylistId = async (playlistId: string) => {
+ try {
+ const commentsRef = collection(db, `PLAYLISTS/${playlistId}/COMMENTS`)
+ const q = query(commentsRef, orderBy('createdAt', 'desc'))
+
+ const querySnapshot = await getDocs(q)
+
+ const comments = await Promise.all(
+ querySnapshot.docs.map(async (commentDoc) => {
+ try {
+ const data = commentDoc.data()
+
+ const createdAt = data.createdAt ? formatDate(data.createdAt.toDate()) : 'Unknown'
+
+ const userRef = data.userRef
+ const userSnapshot = await getDoc(userRef)
+
+ const userData = userSnapshot.exists()
+ ? (userSnapshot.data() as User)
+ : { id: 'Unknown', img: '', name: 'Unknown user' }
+
+ return {
+ id: commentDoc.id,
+ comment: data.comment || 'No comment',
+ createdAt: createdAt,
+ userId: userData.id || 'Unknown user',
+ userImg: userData.img || '',
+ userName: userData.name || 'Unknown user',
+ }
+ } catch (error) {
+ console.error(`Error fetching user data for comment ${commentDoc.id}:`, error)
+ return {
+ id: commentDoc.id,
+ comment: 'No comment',
+ createdAt: 'Unknown',
+ userId: 'Unknown user',
+ userImg: '',
+ userName: 'Unknown user',
+ }
+ }
+ })
+ )
+
+ return comments
+ } catch (error) {
+ console.error('Error fetching comments:', error)
+ return []
+ }
+}
+
+export default getAllCommentsByPlaylistId
diff --git a/src/service/comment/getPaginatedCommentsByPlaylistId.ts b/src/service/comment/getPaginatedCommentsByPlaylistId.ts
new file mode 100644
index 00000000..78e9f3a6
--- /dev/null
+++ b/src/service/comment/getPaginatedCommentsByPlaylistId.ts
@@ -0,0 +1,66 @@
+import {
+ collection,
+ query,
+ getDocs,
+ getDoc,
+ orderBy,
+ limit,
+ startAfter,
+ QueryDocumentSnapshot,
+ DocumentData,
+} from 'firebase/firestore'
+import formatDate from '@/utils/formatDate'
+import { db } from '@/firebase/firebaseConfig'
+
+interface User {
+ id: string
+ img: string
+ name: string
+}
+
+const getPaginatedCommentsByPlaylistId = async (
+ playlistId: string,
+ lastDoc: QueryDocumentSnapshot | null = null,
+ pageSize: number = 5
+) => {
+ try {
+ const commentsRef = collection(db, `PLAYLISTS/${playlistId}/COMMENTS`)
+
+ let q = query(commentsRef, orderBy('createdAt', 'desc'), limit(pageSize))
+
+ if (lastDoc) {
+ q = query(commentsRef, orderBy('createdAt', 'desc'), startAfter(lastDoc), limit(pageSize))
+ }
+
+ const querySnapshot = await getDocs(q)
+
+ const comments = await Promise.all(
+ querySnapshot.docs.map(async (commentDoc) => {
+ const data = commentDoc.data()
+ const createdAt = data.createdAt ? formatDate(data.createdAt.toDate()) : 'Unknown'
+
+ const userRef = data.userRef
+ const userSnapshot = await getDoc(userRef)
+ const userData = userSnapshot.exists()
+ ? (userSnapshot.data() as User)
+ : { id: 'Unknown', img: '', name: 'Unknown user' }
+
+ return {
+ id: commentDoc.id,
+ comment: data.comment || 'No comment',
+ createdAt: createdAt,
+ userId: userData.id || 'Unknown user',
+ userImg: userData.img || '',
+ userName: userData.name || 'Unknown user',
+ }
+ })
+ )
+
+ return { comments, lastDoc: querySnapshot.docs[querySnapshot.docs.length - 1] || null }
+ } catch (error) {
+ console.error('Error fetching comments:', error)
+ return { comments: [], lastDoc: null }
+ }
+}
+
+export default getPaginatedCommentsByPlaylistId
diff --git a/src/service/playlist/createNewPlaylist.ts b/src/service/playlist/createNewPlaylist.ts
new file mode 100644
index 00000000..4d8d6cf2
--- /dev/null
+++ b/src/service/playlist/createNewPlaylist.ts
@@ -0,0 +1,62 @@
+import { db } from '@/firebase/firebaseConfig'
+import { BasicVideoProps } from '@/types/playlistType'
+import { getLoggedInUserUID } from '@/utils/userDataUtils'
+import { addDoc, collection, getDoc, doc } from 'firebase/firestore'
+
+const createNewPlaylist = async (
+ title: string,
+ tags: string[],
+ videoList: BasicVideoProps[],
+ isPrivate: boolean
+) => {
+ try {
+ const userId = await getLoggedInUserUID()
+
+ if (userId) {
+ const userRef = doc(db, 'USERS', userId)
+ const userDoc = await getDoc(userRef)
+ const userName = userDoc.exists() ? userDoc.data()?.name : 'Unknown User'
+
+ const playlistTitle = title || 'Untitled'
+
+ const playlistCollectionRef = collection(db, 'PLAYLISTS')
+ const newPlaylistRef = await addDoc(playlistCollectionRef, {
+ title: playlistTitle,
+ tags: tags,
+ createdAt: new Date(),
+ author: `/${userRef.path}`,
+ authorName: userName,
+ isPrivate: isPrivate,
+ })
+
+ const newPlaylistVideosRef = collection(newPlaylistRef, 'videos')
+ for (const video of videoList) {
+ await addDoc(newPlaylistVideosRef, {
+ title: video.title,
+ thumbnail: video.thumbnail,
+ url: video.url,
+ channelTitle: video.channelTitle,
+ })
+ }
+
+ return {
+ success: true,
+ message: 'Playlist created successfully',
+ playlistId: newPlaylistRef.id,
+ }
+ } else {
+ return {
+ success: false,
+ message: 'User not authenticated',
+ }
+ }
+ } catch (error) {
+ console.error('Error adding playlist: ', error)
+ return {
+ success: false,
+ message: 'Error creating playlist',
+ }
+ }
+}
+
+export default createNewPlaylist
diff --git a/src/service/playlist/getFollowingUsersPlaylists.ts b/src/service/playlist/getFollowingUsersPlaylists.ts
new file mode 100644
index 00000000..7e0658cb
--- /dev/null
+++ b/src/service/playlist/getFollowingUsersPlaylists.ts
@@ -0,0 +1,46 @@
+import { auth, db } from '@/firebase/firebaseConfig'
+import { doc, getDoc } from 'firebase/firestore'
+import getUserPlaylists from '@/service/playlist/getUserPlaylists'
+import { FollowedUserPlaylists } from '@/types/playlistType'
+
+// 현재 로그인된 사용자가 팔로우한 사용자들의 플레이리스트를 한 번에 모두 가져오는 함수
+const getFollowingUsersPlaylists = async (): Promise => {
+ return new Promise((resolve, reject) => {
+ const unsubscribe = auth.onAuthStateChanged(async (user) => {
+ if (user) {
+ try {
+ const uid = user.uid
+ const userRef = doc(db, 'USERS', uid)
+ const userSnap = await getDoc(userRef)
+
+ if (!userSnap.exists()) {
+ console.error('User not found')
+ resolve([])
+ return
+ }
+
+ const userData = userSnap.data()
+ const followingUserIds = userData.following || []
+
+ const followingPlaylists: FollowedUserPlaylists[] = await Promise.all(
+ followingUserIds.map(async (userId: string) => {
+ const userPlaylists = await getUserPlaylists(userId)
+ return { userId, playlists: userPlaylists }
+ })
+ )
+
+ resolve(followingPlaylists)
+ } catch (error) {
+ console.error('Error fetching followed users playlists:', error)
+ reject(error)
+ }
+ } else {
+ console.error('User is not logged in')
+ reject(new Error('User is not logged in'))
+ }
+ unsubscribe()
+ })
+ })
+}
+
+export default getFollowingUsersPlaylists
diff --git a/src/service/playlist/getPaginatedFollowingUsersPlaylists.ts b/src/service/playlist/getPaginatedFollowingUsersPlaylists.ts
new file mode 100644
index 00000000..9fce2c39
--- /dev/null
+++ b/src/service/playlist/getPaginatedFollowingUsersPlaylists.ts
@@ -0,0 +1,57 @@
+import { db, auth } from '@/firebase/firebaseConfig'
+import { doc, getDoc } from 'firebase/firestore'
+import getUserPlaylists from '@/service/playlist/getUserPlaylists'
+import { FollowedUserPlaylists } from '@/types/playlistType'
+
+// 현재 로그인된 사용자가 팔로우한 사용자들의 플레이리스트를 페이징으로 가져오는 함수
+const getPaginatedFollowingUsersPlaylists = async ({
+ startIndex = 0,
+ limit = 3,
+}: {
+ startIndex: number
+ limit: number
+}): Promise => {
+ return new Promise((resolve, reject) => {
+ // auth 상태 변화를 감지하는 함수
+ const unsubscribe = auth.onAuthStateChanged(async (user) => {
+ if (user) {
+ try {
+ const uid = user.uid
+
+ const userRef = doc(db, 'USERS', uid)
+ const userSnap = await getDoc(userRef)
+
+ if (!userSnap.exists()) {
+ console.error('User not found')
+ resolve([]) // 유저가 존재하지 않으면 빈 배열 반환
+ return
+ }
+
+ const userData = userSnap.data()
+ const followingUserIds = userData.following || []
+
+ // 팔로우한 사용자들 중 페이징에 맞는 ID 추출
+ const paginatedFollowingUserIds = followingUserIds.slice(startIndex, startIndex + limit)
+
+ const followingPlaylists = await Promise.all(
+ paginatedFollowingUserIds.map(async (userId: string) => {
+ const userPlaylists = await getUserPlaylists(userId)
+ return { userId, playlists: userPlaylists }
+ })
+ )
+
+ resolve(followingPlaylists) // 플레이리스트 반환
+ } catch (error) {
+ console.error('Error fetching paginated followed users playlists:', error)
+ reject(error)
+ }
+ } else {
+ console.error('User is not logged in')
+ reject(new Error('User is not logged in'))
+ }
+ unsubscribe() // 상태 변화 감지 중지
+ })
+ })
+}
+
+export default getPaginatedFollowingUsersPlaylists
diff --git a/src/service/playlist/getUserPlaylistDetails.ts b/src/service/playlist/getUserPlaylistDetails.ts
new file mode 100644
index 00000000..81ce271c
--- /dev/null
+++ b/src/service/playlist/getUserPlaylistDetails.ts
@@ -0,0 +1,65 @@
+import { db } from '@/firebase/firebaseConfig'
+import { collection, getDocs, getDoc, doc } from 'firebase/firestore'
+import formatDate from '@/utils/formatDate'
+
+// 주어진 플레이리스트 ID에 대한 세부 정보를 가져오는 함수
+const getUserPlaylistDetails = async (playlistId: string) => {
+ try {
+ const playlistInfoRef = doc(db, 'PLAYLISTS', playlistId)
+ const infoSnapshot = await getDoc(playlistInfoRef)
+
+ if (!infoSnapshot.exists()) {
+ console.error('Playlist not found')
+ return []
+ }
+
+ const playlistData = infoSnapshot.data()
+
+ const authorUID = playlistData?.author?.split('/').pop()
+ const userDocRef = doc(db, 'USERS', authorUID)
+ const userDoc = await getDoc(userDocRef)
+ const userData = userDoc.data()
+
+ const videosRef = collection(db, `PLAYLISTS/${playlistId}/videos`)
+ const videoSnapshot = await getDocs(videosRef)
+
+ const videos = await Promise.all(
+ videoSnapshot.docs.map(async (videoDoc) => {
+ try {
+ const videoData = videoDoc.data()
+ return {
+ channelTitle: videoData.channelTitle,
+ title: videoData.title,
+ thumbnail: videoData.thumbnail,
+ url: videoData.url,
+ author: userData?.id,
+ authorImg: userData?.img,
+ uploadDate: playlistData.createdAt ? formatDate(playlistData.createdAt) : '',
+ tags: playlistData.tags,
+ playlistTitle: playlistData.title,
+ }
+ } catch (error) {
+ console.error(`Error fetching video data for ${videoDoc.id}:`, error)
+ return {
+ channelTitle: 'Unknown',
+ title: 'Unknown Video',
+ thumbnail: 'not valid thumbnail',
+ url: '',
+ author: userData?.id,
+ authorImg: userData?.img,
+ uploadDate: '',
+ tags: [],
+ playlistTitle: playlistData.title,
+ }
+ }
+ })
+ )
+
+ return videos
+ } catch (error) {
+ console.error('Error fetching playlist details:', error)
+ return []
+ }
+}
+
+export default getUserPlaylistDetails
diff --git a/src/service/playlist/getUserPlaylists.ts b/src/service/playlist/getUserPlaylists.ts
new file mode 100644
index 00000000..7c8e6e0d
--- /dev/null
+++ b/src/service/playlist/getUserPlaylists.ts
@@ -0,0 +1,72 @@
+import { db } from '@/firebase/firebaseConfig'
+import { getUserRef } from '@/utils/userDataUtils'
+import { collection, query, where, getDocs, doc as firestoreDoc, getDoc } from 'firebase/firestore'
+import formatDate from '@/utils/formatDate'
+import defaultThumbnail from '@/assets/default-thumbnail.png'
+import NPProfile from '@/assets/np_logo.svg'
+
+// 특정 사용자 또는 로그인한 사용자의 플레이리스트를 가져오는 함수
+const getUserPlaylists = async (userId?: string) => {
+ try {
+ let userRef
+ if (userId) {
+ userRef = `/USERS/${userId}`
+ } else {
+ userRef = await getUserRef()
+ }
+
+ const playlistQuery = query(collection(db, 'PLAYLISTS'), where('author', '==', userRef))
+ const playlistQuerySnapshot = await getDocs(playlistQuery)
+
+ const playlistsArray = await Promise.all(
+ playlistQuerySnapshot.docs.map(async (playlistDoc) => {
+ try {
+ const playlistData = playlistDoc.data()
+
+ const authorId = playlistData.author.split('/')[2]
+
+ const authorRef = firestoreDoc(db, 'USERS', authorId)
+ const authorSnapshot = await getDoc(authorRef)
+
+ const authorData = authorSnapshot.exists()
+ ? {
+ img: authorSnapshot.data()?.img || defaultThumbnail, // Default if img doesn't exist
+ id: authorSnapshot.data()?.id || 'Unknown', // Default if id doesn't exist
+ }
+ : { img: defaultThumbnail, id: 'Unknown' }
+ const videosRef = collection(playlistDoc.ref, 'videos')
+ const videoSnapshot = await getDocs(videosRef)
+
+ const thumbnails = videoSnapshot.docs.slice(0, 4).map((videoDoc) => {
+ return videoDoc.data().thumbnail || 'not valid thumbnail'
+ })
+
+ const createdAt = playlistData.createdAt ? formatDate(playlistData.createdAt) : 'Unknown'
+
+ return {
+ playlistId: playlistDoc.id,
+ title: playlistData.title || 'Untitled',
+ thumbnails: thumbnails,
+ isPrivate: playlistData.isPrivate || false,
+ createdAt: createdAt,
+ authorId: authorData.id || 'Unknown',
+ authorName: playlistData.authorName || 'Unknown',
+ authorImg: authorData.img || NPProfile,
+ likers: playlistData.likers || [],
+ tags: playlistData.tags || [],
+ }
+ } catch (error) {
+ console.error(`Error fetching videos for playlist ${playlistDoc.id}:`, error)
+ return null
+ }
+ })
+ )
+
+ return playlistsArray.filter((playlist) => playlist !== null)
+ } catch (error) {
+ console.error(`Error fetching playlists for user ${userId || 'logged-in user'}:`, error)
+ return []
+ }
+}
+
+export default getUserPlaylists
diff --git a/src/service/playlist/likePlaylist.ts b/src/service/playlist/likePlaylist.ts
new file mode 100644
index 00000000..fcfe6555
--- /dev/null
+++ b/src/service/playlist/likePlaylist.ts
@@ -0,0 +1,48 @@
+import { useState, useEffect } from 'react'
+import { useMutation } from '@tanstack/react-query'
+import { doc, onSnapshot, updateDoc, arrayRemove, arrayUnion } from 'firebase/firestore'
+import { auth, db } from '@/firebase/firebaseConfig'
+
+export const useLikeButton = (playlistId: string) => {
+ const [likeData, setLikeData] = useState({ likers: [] as string[], likeCount: 0 })
+ const likeDocRef = doc(db, `PLAYLISTS/${playlistId}`)
+
+ useEffect(() => {
+ const unsubscribe = onSnapshot(likeDocRef, (docSnapshot) => {
+ if (docSnapshot.exists()) {
+ const data = docSnapshot.data()
+ setLikeData({
+ likers: data.likers || [],
+ likeCount: data.likers?.length || 0,
+ })
+ } else {
+ setLikeData({ likers: [], likeCount: 0 })
+ }
+ })
+
+ return () => unsubscribe()
+ }, [playlistId])
+
+ const currentUserId = auth.currentUser?.uid
+ const isLiked = currentUserId ? likeData.likers.includes(currentUserId) : false
+ const { likeCount } = likeData
+
+ const likeMutation = useMutation({
+ mutationFn: async () => {
+ const userId = auth.currentUser?.uid
+ if (userId) {
+ await updateDoc(likeDocRef, {
+ likers: isLiked ? arrayRemove(userId) : arrayUnion(userId),
+ })
+ }
+ },
+ })
+
+ const toggleLike = () => {
+ if (auth.currentUser) {
+ likeMutation.mutate()
+ }
+ }
+
+ return { isLiked, likeCount, toggleLike }
+}
diff --git a/src/service/profile/followService.ts b/src/service/profile/followService.ts
new file mode 100644
index 00000000..2a36de59
--- /dev/null
+++ b/src/service/profile/followService.ts
@@ -0,0 +1,48 @@
+import { db } from '@/firebase/firebaseConfig'
+import { getUIDFromUserId } from '@/utils/userDataUtils'
+import { doc, updateDoc, arrayRemove, arrayUnion, onSnapshot } from 'firebase/firestore'
+
+export const followUser = async (
+ targetUserId: string,
+ currentUID: string,
+ isFollowing: boolean
+) => {
+ const targetUID = await getUIDFromUserId(targetUserId)
+ const targetUserDocRef = doc(db, `USERS/${targetUID}`)
+ const currentUserDocRef = doc(db, `USERS/${currentUID}`)
+
+ const updates = [
+ updateDoc(targetUserDocRef, {
+ followers: isFollowing ? arrayRemove(currentUID) : arrayUnion(currentUID),
+ }),
+
+ updateDoc(currentUserDocRef, {
+ following: isFollowing ? arrayRemove(targetUID) : arrayUnion(targetUID),
+ }),
+ ]
+ await Promise.all(updates)
+}
+
+export const followStatus = async (
+ targetUserId: string,
+ currentUID: string,
+ onUpdate: (data: { followers: string[]; following: string[] }, isFollowing: boolean) => void
+) => {
+ const targetUID = await getUIDFromUserId(targetUserId)
+ const targetUserDocRef = doc(db, `USERS/${targetUID}`)
+
+ const unsubscribe = onSnapshot(targetUserDocRef, (docSnapshot) => {
+ if (docSnapshot.exists()) {
+ const data = docSnapshot.data()
+ onUpdate(
+ {
+ followers: data.followers || [],
+ following: data.followings || [],
+ },
+ data.followers?.includes(currentUID) || false
+ )
+ }
+ })
+
+ return unsubscribe
+}
diff --git a/src/service/profile/profileInfo.ts b/src/service/profile/profileInfo.ts
new file mode 100644
index 00000000..4b5ffbb3
--- /dev/null
+++ b/src/service/profile/profileInfo.ts
@@ -0,0 +1,60 @@
+import { auth, db } from '@/firebase/firebaseConfig'
+import { collection, doc, getDocs, getDoc, query, where } from 'firebase/firestore'
+import { UserType } from '@/types/userType'
+import { getUIDFromUserId } from '@/utils/userDataUtils'
+
+export interface ProfileProps extends UserType {
+ followers: FollowerProps[]
+ following: FollowingProps[]
+}
+
+export interface FollowerProps {
+ userId: string
+ img: string | null
+}
+
+export interface FollowingProps {
+ userId: string
+ img: string | null
+}
+
+export const userInfo = async (userId?: string) => {
+ try {
+ let uid = auth.currentUser?.uid
+
+ if (userId && userId !== uid) {
+ uid = await getUIDFromUserId(userId)
+ }
+
+ if (uid) {
+ const userRef = doc(db, 'USERS', uid)
+ const userDoc = await getDoc(userRef)
+
+ if (!userDoc.exists()) {
+ return
+ }
+
+ const userData = userDoc.data() as ProfileProps
+ const [querySnapShot] = await Promise.all([
+ getDocs(query(collection(db, 'PLAYLISTS'), where('author', '==', `/USERS/${uid}`))),
+ ])
+
+ const playlistLength = querySnapShot.size
+ const followers = userData.followers || []
+ const followings = userData.following || []
+
+ return {
+ userName: userData.name,
+ userId: userData.id,
+ userImg: userData.img,
+ userEmail: userData.email,
+ userBio: userData.bio,
+ followerLength: followers.length,
+ followingLength: followings.length,
+ playlistLength,
+ }
+ }
+ } catch (error) {
+ console.error('userInfo fetch Error', error)
+ }
+}
diff --git a/src/service/search/getRecommendTags.ts b/src/service/search/getRecommendTags.ts
new file mode 100644
index 00000000..1761675a
--- /dev/null
+++ b/src/service/search/getRecommendTags.ts
@@ -0,0 +1,33 @@
+import { db } from '@/firebase/firebaseConfig'
+import { collection, getDocs } from 'firebase/firestore'
+
+const getRandomElements = (arr: string[], n: number) => {
+ const shuffled = [...arr].sort(() => 0.5 - Math.random())
+ return shuffled.slice(0, n)
+}
+
+const getRecommendTags = async (): Promise => {
+ try {
+ const tagsQuery = collection(db, 'PLAYLISTS')
+ const querySnapshot = await getDocs(tagsQuery)
+
+ let allTags: string[] = []
+
+ querySnapshot.forEach((doc) => {
+ const data = doc.data()
+ if (data.isPrivate === false && data.tags && Array.isArray(data.tags)) {
+ allTags = allTags.concat(data.tags)
+ }
+ })
+
+ const uniqueTags = [...new Set(allTags)]
+ const randomTags = getRandomElements(uniqueTags, 12)
+
+ return randomTags
+ } catch (error) {
+ console.error('Error getting tags:', error)
+ return []
+ }
+}
+
+export default getRecommendTags
diff --git a/src/service/search/searchTag.ts b/src/service/search/searchTag.ts
new file mode 100644
index 00000000..ce3efa3c
--- /dev/null
+++ b/src/service/search/searchTag.ts
@@ -0,0 +1,28 @@
+import { db } from '@/firebase/firebaseConfig'
+import { collection, query, where, getDocs } from 'firebase/firestore'
+
+export interface Playlist {
+ thumbnails: string[]
+ id: string
+ title: string
+ tags: string[]
+}
+
+export const SearchTag = async (tag: string): Promise => {
+ const formattedTag = `${tag.trim()}`
+ const playlistsRef = collection(db, 'PLAYLISTS')
+ const q = query(
+ playlistsRef,
+ where('tags', 'array-contains', formattedTag),
+ where('isPrivate', '==', false)
+ )
+
+ const querySnapshot = await getDocs(q)
+ const results: Playlist[] = []
+ querySnapshot.forEach((doc) => {
+ const playlist = { id: doc.id, ...doc.data() } as Playlist
+ results.push(playlist)
+ })
+
+ return results
+}
diff --git a/src/stores/useAuthStore.tsx b/src/stores/useAuthStore.tsx
new file mode 100644
index 00000000..1398cb97
--- /dev/null
+++ b/src/stores/useAuthStore.tsx
@@ -0,0 +1,13 @@
+import create from 'zustand'
+
+interface AuthStore {
+ isAuthenticated: boolean
+ setAuthenticated: (auth: boolean) => void
+}
+
+const useAuthStore = create((set) => ({
+ isAuthenticated: false,
+ setAuthenticated: (auth: boolean) => set({ isAuthenticated: auth }),
+}))
+
+export default useAuthStore
diff --git a/src/styles/GlobalStyles.tsx b/src/styles/GlobalStyles.tsx
new file mode 100644
index 00000000..936669aa
--- /dev/null
+++ b/src/styles/GlobalStyles.tsx
@@ -0,0 +1,56 @@
+import { Global, css } from '@emotion/react'
+import 'normalize.css'
+import { colors } from '@/styles/colors'
+
+const GlobalStyles = () => (
+
+)
+
+export default GlobalStyles
diff --git a/src/styles/colors.ts b/src/styles/colors.ts
new file mode 100644
index 00000000..550d5345
--- /dev/null
+++ b/src/styles/colors.ts
@@ -0,0 +1,15 @@
+export const colors = {
+ white: '#FFFFFF',
+ red: '#EE1010',
+ primaryPurple: '#612FF1',
+ primaryPurpleHover: '#5C33D0',
+ lightPurPle: 'rgba(97, 47, 241, 0.1)',
+ mediumPurple: '#987bef',
+ lightestGray: '#F7F7F7',
+ brightGray: '#EEEEEE',
+ brightGrayHover: '#E4E4E4',
+ lightGray: '#DDDDDD',
+ gray: '#CDCDCD',
+ darkGray: '#6F6F6F',
+ black: '#333333',
+}
diff --git a/src/styles/font.ts b/src/styles/font.ts
new file mode 100644
index 00000000..7996bec5
--- /dev/null
+++ b/src/styles/font.ts
@@ -0,0 +1,23 @@
+export const fontSize = {
+ xxxxl: '32px',
+ xxxl: '28px',
+ xxl: '24px',
+ xl: '20px',
+ lg: '18px',
+ md: '16px',
+ sm: '14px',
+ xs: '12px',
+ xxs: '11px',
+}
+
+export const fontWeight = {
+ thin: 100,
+ extraLight: 200,
+ light: 300,
+ regular: 400,
+ medium: 500,
+ semiBold: 600,
+ bold: 700,
+ extraBold: 800,
+ black: 900,
+}
diff --git a/src/types/commentType.d.ts b/src/types/commentType.d.ts
new file mode 100644
index 00000000..f6d63735
--- /dev/null
+++ b/src/types/commentType.d.ts
@@ -0,0 +1,8 @@
+export interface CommentType {
+ userId: string
+ id: string
+ comment: string
+ createdAt: Timestamp | number | Date
+ userName: string
+ userImg: string
+}
diff --git a/src/types/formType.d.ts b/src/types/formType.d.ts
new file mode 100644
index 00000000..eaebdde1
--- /dev/null
+++ b/src/types/formType.d.ts
@@ -0,0 +1,10 @@
+export interface FormInputs {
+ email: string
+ password: string
+}
+
+export interface FormProps {
+ title: string
+ getDataForm: (data: FormInputs) => void
+ firebaseError?: string
+}
diff --git a/src/types/playlistType.d.ts b/src/types/playlistType.d.ts
new file mode 100644
index 00000000..2fd0c9b5
--- /dev/null
+++ b/src/types/playlistType.d.ts
@@ -0,0 +1,37 @@
+export type filterPlaylist = 'all' | 'public' | 'private'
+export interface BasicVideoProps {
+ title: string
+ channelTitle: string
+ url: string
+ thumbnail: string
+}
+
+export interface ExtendedVideoProps extends BasicVideoProps {
+ author: string
+ authorImg?: string
+ uploadDate: string
+ tags: string[]
+ playlistTitle?: string
+ likes?: number
+}
+
+export interface PlaylistBaseProps {
+ playlistId: string
+ title: string
+ thumbnail: string[]
+ isPrivate: boolean
+ createdAt: string
+}
+
+export interface videoListProps extends PlaylistBaseProps, ExtendedVideoProps {}
+
+export interface FollowedPlaylist extends PlaylistBaseProps {
+ thumbnails: string[]
+ authorName: string
+ authorImg?: string
+ authorId?: string
+}
+
+export interface FollowedUserPlaylists {
+ playlists: Playlist[]
+}
diff --git a/src/types/userType.d.ts b/src/types/userType.d.ts
new file mode 100644
index 00000000..2379678d
--- /dev/null
+++ b/src/types/userType.d.ts
@@ -0,0 +1,7 @@
+export interface UserType {
+ name?: string
+ bio?: string
+ email?: string
+ img?: string
+ id?: string
+}
diff --git a/src/utils/formatDate.ts b/src/utils/formatDate.ts
new file mode 100644
index 00000000..e207a507
--- /dev/null
+++ b/src/utils/formatDate.ts
@@ -0,0 +1,26 @@
+import { Timestamp } from 'firebase/firestore'
+
+const formatDate = (timestamp: Timestamp | number | Date) => {
+ let date: Date
+
+ if (timestamp instanceof Timestamp) {
+ date = timestamp.toDate()
+ } else if (typeof timestamp === 'number') {
+ date = new Date(timestamp)
+ } else if (timestamp instanceof Date) {
+ date = timestamp
+ } else {
+ console.error('Invalid timestamp format')
+ return 'Invalid Date'
+ }
+
+ const year = date.getFullYear()
+ const month = date.getMonth() + 1
+ const day = date.getDate()
+ const hours = date.getHours().toString().padStart(2, '0')
+ const minutes = date.getMinutes().toString().padStart(2, '0')
+
+ return `${year}. ${month}. ${day}. ${hours}:${minutes}`
+}
+
+export default formatDate
diff --git a/src/utils/userDataUtils.ts b/src/utils/userDataUtils.ts
new file mode 100644
index 00000000..5d6e05ce
--- /dev/null
+++ b/src/utils/userDataUtils.ts
@@ -0,0 +1,49 @@
+import { db } from '@/firebase/firebaseConfig'
+import { getAuth } from 'firebase/auth'
+import { collection, doc, getDoc, getDocs, query, where } from 'firebase/firestore'
+
+// 주어진 UID에 해당하는 사용자의 userId를 반환하는 함수
+export const getUserIdFromUID = async (uid: string) => {
+ const userRef = doc(db, 'USERS', uid)
+ const userDoc = await getDoc(userRef)
+
+ if (!userDoc.exists()) {
+ throw new Error('User document not found')
+ }
+ const userData = userDoc.data()
+ return userData?.id
+}
+
+// 주어진 userId에 해당하는 사용자의 UID를 반환하는 함수
+export const getUIDFromUserId = async (userId?: string) => {
+ const userQuery = query(collection(db, 'USERS'), where('id', '==', userId))
+ const querySnapshot = await getDocs(userQuery)
+
+ if (!querySnapshot.empty) {
+ return querySnapshot.docs[0].id
+ }
+ throw new Error('User not found')
+}
+
+// 현재 로그인된 사용자의 UID(고유 식별자)를 반환하는 함수
+export const getLoggedInUserUID = (): string => {
+ const auth = getAuth()
+ const user = auth.currentUser
+
+ if (user) {
+ return user.uid
+ } else {
+ return ''
+ }
+}
+
+// 현재 로그인된 사용자의 Firestore 참조 경로를 반환하는 함수
+export const getUserRef = (): string => {
+ const uid = getLoggedInUserUID()
+
+ if (uid) {
+ return `/USERS/${uid}`
+ } else {
+ throw new Error('No logged in user')
+ }
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
deleted file mode 100644
index d739292a..00000000
--- a/tsconfig.app.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
- "target": "ES2020",
- "useDefineForClassFields": true,
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
- "jsx": "react-jsx",
-
- /* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src"]
-}
diff --git a/tsconfig.json b/tsconfig.json
index ea9d0cd8..d1bbd2ef 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,11 +1,32 @@
{
- "files": [],
- "references": [
- {
- "path": "./tsconfig.app.json"
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
},
- {
- "path": "./tsconfig.node.json"
- }
- ]
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "jsxImportSource": "@emotion/react",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
}
diff --git a/tsconfig.node.json b/tsconfig.node.json
deleted file mode 100644
index 3afdd6e3..00000000
--- a/tsconfig.node.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "composite": true,
- "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
- "skipLibCheck": true,
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowSyntheticDefaultImports": true,
- "strict": true,
- "noEmit": true
- },
- "include": ["vite.config.ts"]
-}
diff --git a/vite.config.ts b/vite.config.ts
index 5a33944a..2f6343f4 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -4,4 +4,9 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ '@': '/src',
+ },
+ },
})