diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000..a04734de --- /dev/null +++ b/.cursorrules @@ -0,0 +1,75 @@ +You are an expert in React, JavaScript, Vite, Material UI, Clerk Auth, React Router, Zustand, Tanstack React Query, axios, moment, Tabler icons, MUI icons, Vercel, Express.js, Node.js, Cloudinary, Prisma, and Neon PostgreSQL. + +Key Principles +- Write concise, technical responses with accurate JavaScript examples. +- Use functional, declarative programming. Avoid classes. +- Prefer iteration and modularization over duplication. +- Use descriptive variable names with auxiliary verbs (e.g., isLoading). +- Use lowercase with dashes for directories (e.g., components/auth-modal). +- Favor named exports for components. +- Use the Receive an Object, Return an Object (RORO) pattern. + +JavaScript +- Use "function" keyword for pure functions. Use semicolons. +- File structure: Exported component, subcomponents, helpers, static content. +- Avoid unnecessary curly braces in conditional statements. +- For single-line statements in conditionals, omit curly braces. + +Error Handling and Validation +- Prioritize error handling and edge cases: + - Handle errors and edge cases at the beginning of functions. + - Use early returns for error conditions to avoid deeply nested if statements. + - Place the happy path last in the function for improved readability. + - Avoid unnecessary else statements; use if-return pattern instead. + - Use guard clauses to handle preconditions and invalid states early. + - Implement proper error logging and user-friendly error messages. + - Consider using custom error types or error factories for consistent error handling. + +React/Vite +- Use functional components. +- Use declarative JSX. +- Use const, not function, for components. +- Use Material UI for components and styling. +- Implement responsive design with Material UI's responsive utilities. +- Use mobile-first approach for responsive design. +- Place static content at file end. +- Use content variables for static content outside render functions. +- Use Clerk for authentication and user management. +- Use axios for API requests. +- Use React Router for navigation. +- Use Zustand for state management. +- Utilize Tanstack React Query for data fetching and caching. +- Use moment for date and time manipulation. +- Use Tabler icons and MUI icons for iconography. +- Deploy frontend to Vercel. + +Backend (Express.js/Node.js) +- Use Express.js for routing and middleware. +- Implement RESTful API design principles. +- Use Prisma as the ORM for database operations. +- Implement proper error handling middleware. +- Use environment variables for configuration. +- Implement input validation and sanitization. +- Use async/await for asynchronous operations. +- Use Clerk for user authorization on the backend. +- Integrate Cloudinary for image and file management. + +Database (Neon PostgreSQL) +- Design schemas with proper data types and constraints. +- Use indexes for frequently queried fields. +- Implement data pagination for large datasets. +- Use Prisma migrations for database schema management. + +Key Conventions +1. Use Vite for fast development and optimized builds. +2. Prioritize Web Vitals (LCP, CLS, FID). +3. Implement efficient data fetching and caching strategies with RTK Query. +4. Use proper security measures (e.g., CORS, helmet for Express). +5. Utilize Prisma's type-safe database access. +6. Implement proper state management with Zustand. +7. Use React Router for declarative routing. +8. Leverage Clerk's authentication and user management features. +9. Optimize image delivery with Cloudinary. +10. Utilize Neon PostgreSQL's scalability and performance features. + +Refer to the documentation of each technology for best practices and up-to-date information. \ No newline at end of file diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 919d5109..00000000 --- a/.deepsource.toml +++ /dev/null @@ -1,28 +0,0 @@ -version = 1 - -test_patterns = ["*/test/**"] - -exclude_patterns = [ - "public/**,", - "dist/**" -] - -[[analyzers]] -name = "shell" - -[[analyzers]] -name = "javascript" - - [analyzers.meta] - plugins = ["react"] - environment = [ - "mongo", - "nodejs", - "browser" - ] - -[[transformers]] -name = "prettier" - -[[transformers]] -name = "standardjs" \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/1-feature-request.yml b/.github/ISSUE_TEMPLATE/1-feature-request.yml new file mode 100644 index 00000000..1bb66cff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-feature-request.yml @@ -0,0 +1,38 @@ +name: πŸ’‘ General Feature Request +description: Have a new idea/feature for memories? Please suggest! +labels: ['⭐ goal: addition', 'good first issue', '🀩 Up for the grab', 'help wanted', 'enhancement'] +title: '[FEATURE πŸ’‘] ' +body: + - type: textarea + id: description + attributes: + label: Description + description: A brief description of the enhancement you propose, also include what you tried and what worked. + placeholder: Enter your description + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Screenshots to support proposal of feature or idea + placeholder: Please attach screenshots if applicable + validations: + required: false + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this idea? + value: | + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** + ## Happy contributing. πŸ’ + Star my other Repositories [here](https://github.com/warmachine028) + validations: + required: false + - type: markdown + attributes: + value: | + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** + ## Happy contributing. πŸ’ + Star my other Repositories [here](https://github.com/warmachine028?tab=repositories) diff --git a/.github/ISSUE_TEMPLATE/2-bug.yml b/.github/ISSUE_TEMPLATE/2-bug.yml new file mode 100644 index 00000000..50ec52ff --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-bug.yml @@ -0,0 +1,38 @@ +name: πŸ› Bug Bounty Hunter +description: Report an bug in memories and help improve the project! +labels: ['πŸ›  goal: fix', 'good first issue', '🀩 Up for the grab', 'help wanted', 'enhancement', 'bug'] +title: '[BUG 🐞] ' +body: + - type: textarea + id: description + attributes: + label: Description + description: A brief description of the question or issue, also include what you tried and what didn't work + placeholder: Enter your description + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Screenshots to support replication of Bug + placeholder: Please attach screenshots if applicable + validations: + required: false + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this idea? + value: | + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** + ## Happy contributing. πŸ’ + Star my other Repositories [here](https://github.com/warmachine028) + validations: + required: false + - type: markdown + attributes: + value: | + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** + ## Happy contributing. πŸ’ + Star my other Repositories [here](https://github.com/warmachine028?tab=repositories) diff --git a/.github/ISSUE_TEMPLATE/3-docs.yml b/.github/ISSUE_TEMPLATE/3-docs.yml new file mode 100644 index 00000000..f19d6ef0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3-docs.yml @@ -0,0 +1,30 @@ +name: πŸ“„ Documentation issue +description: Found an issue in the documentation? You can use this one! +title: '[DOCS πŸ“„] ' +labels: ['πŸ“„ aspect: text', 'good first issue', '🀩 Up for the grab'] +body: + - type: textarea + id: description + attributes: + label: Description + description: A brief description of the question or issue, also include what you tried and what didn't work + validations: + required: true + - type: textarea + id: screenshots + attributes: + label: Screenshots + description: Please add screenshots if applicable + validations: + required: false + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this issue? + validations: + required: false + - type: markdown + attributes: + value: | + Star my other Repositories [here](https://github.com/warmachine028) diff --git a/.github/ISSUE_TEMPLATE/4-other.yml b/.github/ISSUE_TEMPLATE/4-other.yml new file mode 100644 index 00000000..5952f7c0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4-other.yml @@ -0,0 +1,26 @@ +name: Other +description: Use this for any other issues. Please do NOT create blank issues +title: '[OTHER]' +labels: ['🚦 status: awaiting triage'] +body: + - type: markdown + attributes: + value: '# Other issue' + - type: textarea + id: issuedescription + attributes: + label: What would you like to share? + description: Provide a clear and concise explanation of your issue. + validations: + required: true + - type: textarea + id: extrainfo + attributes: + label: Additional information + description: Is there anything else we should know about this issue? + validations: + required: false + - type: markdown + attributes: + value: | + Star my other Repositories [here](https://github.com/warmachine028) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index aa6937a4..49c16c95 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -26,7 +26,7 @@ body: description: Is there anything else we should know about this idea? value: | - Always use styled components instead of plain CSS and MUI components. - ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and [SETUP](https://github.com/warmachine028/memories/tree/main/rules) docs before proceeding** + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** ## Happy contributing. πŸ’ Star my other Repositories [here](https://github.com/warmachine028) validations: @@ -34,7 +34,7 @@ body: - type: markdown attributes: value: | - ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and [SETUP](https://github.com/warmachine028/memories/tree/main/rules) docs before proceeding** + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** ## Happy contributing. πŸ’ Star my other Repositories [here](https://github.com/warmachine028?tab=repositories) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0dec64bb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: πŸ™‹πŸΎπŸ™‹πŸΌβ€Question + url: https://github.com/warmachine028 + about: Feel free to ask your question on Discussion. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index e1a0bae5..37bac853 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -26,7 +26,7 @@ body: description: Is there anything else we should know about this idea? value: | - Always use styled components instead of plain CSS and MUI components. - ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and [SETUP](https://github.com/warmachine028/memories/tree/main/rules) docs before proceeding** + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** ## Happy contributing. πŸ’ Star my other Repositories [here](https://github.com/warmachine028) validations: @@ -34,7 +34,7 @@ body: - type: markdown attributes: value: | - ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and [SETUP](https://github.com/warmachine028/memories/tree/main/rules) docs before proceeding** + ## **Make sure to read the [CONTRIBUTING](https://github.com/warmachine028/memories/blob/main/CONTRIBUTING.md) and docs before proceeding** ## Happy contributing. πŸ’ Star my other Repositories [here](https://github.com/warmachine028?tab=repositories) diff --git a/.github/actions/push-changes/action.yml b/.github/actions/push-changes/action.yml new file mode 100644 index 00000000..bce74184 --- /dev/null +++ b/.github/actions/push-changes/action.yml @@ -0,0 +1,23 @@ +name: Push changes +description: A reusable action to push changes with a specified commit message and file paths +inputs: + message: + description: Commit message + required: true + paths: + description: File paths to add + default: '.' +runs: + using: composite + steps: + - name: Pull changes + run: git pull + shell: bash + + - name: Push changes + uses: EndBug/add-and-commit@v9 + with: + committer_name: 'GitHub Actions' + committer_email: 'actions@github.com' + add: ${{ inputs.paths }} + message: '${{ inputs.message }}' diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7d3dfef5..825a3366 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,17 +1,19 @@ version: 2 updates: - - package-ecosystem: github-actions - directory: ".github" - schedule: - interval: daily - time: "21:00" - - package-ecosystem: npm - directory: "client" - schedule: - interval: daily - time: "21:00" - - package-ecosystem: npm - directory: "server" - schedule: - interval: daily - time: "21:00" + - package-ecosystem: github-actions #! for github-actions + directory: '.github' + schedule: + interval: daily + time: '21:00' + - package-ecosystem: npm #! enable for javascript & typescript + open-pull-requests-limit: 10 + directory: 'client' + schedule: + interval: daily + time: '21:00' + - package-ecosystem: npm #! enable for javascript & typescript + open-pull-requests-limit: 10 + directory: 'server' + schedule: + interval: daily + time: '21:00' diff --git a/.github/labels.js b/.github/labels.js new file mode 100644 index 00000000..6dace54c --- /dev/null +++ b/.github/labels.js @@ -0,0 +1,187 @@ +const updateLabel = label => { + let flag = false + ;[].slice.call(document.querySelectorAll(".js-labels-list-item")).forEach(element => { + if (element.querySelector(".js-label-link").textContent.trim() === label.name) { + flag = true + element.querySelector(".js-edit-label").click() + element.querySelector(".js-new-label-name-input").value = label.name + element.querySelector(".js-new-label-description-input").value = label.description ? label.description : '' + element.querySelector(".js-new-label-color-input").value = `#${label.color}` + element.querySelector(".js-edit-label-cancel ~ .btn-primary").click() + } + }) + return flag +} + +const addNewLabel = label => { + document.querySelector(".js-new-label-name-input").value = label.name + document.querySelector(".js-new-label-description-input").value = label.description ? label.description : '' + document.querySelector(".js-new-label-color-input").value = `#${label.color}` + document.querySelector(".js-details-target ~ .btn-primary").disabled = false + document.querySelector(".js-details-target ~ .btn-primary").click() +} + +const addLabel = label => { + if (!updateLabel(label)) { + addNewLabel(label) + } +} + +// Your labels +const labels = [ + { + name: "good first issue", + description: "The issue is to encourage first time contributors", + color: "7f0799" + }, + { + name: 'github_actions', + description: 'The PR was opend by an automated github action', + color: '12161c' + }, + { + name: "🀩 Up for the grab", + description: "The issue is ready to be assigned to a contributor", + color: "6C049F" + }, + { + name: "help wanted", + color: "7f0799", + }, + { + name: "duplicate", + color: "7f0799", + }, + { + name: "⛔️ status: discarded", + color: "eeeeee", + }, + { + name: "✨ goal: improvement", + color: "ffffff", + }, + { + name: "❓ talk: question", + color: "f9bbe5", + }, + { + name: "⭐ goal: addition", + color: "ffffff", + }, + { + name: "🏁 status: ready for dev", + color: "cccccc", + }, + { + name: "πŸ’¬ talk: discussion", + color: "f9bbe5", + }, + { + name: "πŸ’» aspect: code", + color: "04338c", + }, + { + name: "πŸ“„ aspect: text", + color: "04338c", + }, + { + name: "πŸ”’ staff only", + color: "7f0799", + }, + { + name: "πŸ•Ή aspect: interface", + color: "04338c", + }, + { + name: "🚦 status: awaiting triage", + color: "333333", + }, + { + name: "🚧 status: blocked", + color: "999999", + }, + { + name: "πŸ›  goal: fix", + color: "ffffff", + }, + { + name: "πŸŸ₯ priority: critical", + color: "b60205", + }, + { + name: "🟧 priority: high", + color: "ff9f1c", + }, + { + name: "🟨 priority: medium", + color: "ffcc00", + }, + { + name: "🟩 priority: low", + color: "cfda2c", + }, + { + name: "πŸ€– aspect: dx", + color: "04338c", + }, + { + name: "🧹 status: ticket work required", + color: "666666", + }, + { + name: "πŸ™… status: discontinued", + color: "cccccc", + }, + { + name: "🏷 status: label work required", + color: "666666", + }, + { + name: "πŸ”’ points: 1", + color: "62A1A6", + }, + { + name: "πŸ”’ points: 2", + color: "62A1A6", + }, + { + name: "πŸ”’ points: 3", + color: "62A1A6", + }, + { + name: "πŸ”’ points: 5", + color: "62A1A6", + }, + { + name: "πŸ”’ points: 8", + color: "62A1A6", + }, + { + name: "πŸ”’ points: 13", + color: "62A1A6", + }, + { + name: "no-issue-activity", + color: "ededed", + }, + { + name: "dependencies", + color: "0366d6", + }, + { + name: "hacktoberfest", + description: "This issue/pull request is specially marked for hacktoberfest", + color: "eb06b0", + }, + { + name: "hacktoberfest-accepted", + description: "The contribution was accepted for hactoberfest", + color: "0f8b16", + }, + { + name: "feature", + description: "This issue/PR has will be a new feature", + color: "838b0f", + }, +] +labels.forEach(label => addLabel(label)) diff --git a/.github/labels.yml b/.github/labels.yml index 24b115f7..82601e72 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -4,11 +4,13 @@ filters: regexs: - /feat/i events: [pull_request] + targets: [title] - - label: "πŸ’‘feature" + - label: "feature" regexs: - /feat/i events: [pull_request] + targets: [title] # For Docs - label: "documentation" @@ -26,21 +28,29 @@ filters: # For Bugs/Fixes - label: "πŸ›  goal: fix" regexs: - - /fix/i - - /refactor/i + - /^fix*/i + - /bug/i events: [pull_request] targets: [title] - label: "🐞 bug" regexs: - - /fix/i - - /refactor/i + - /^fix*/i + - /bug/i events: [pull_request] targets: [title] - # For Hacktoberfest only - - label: "hacktoberfest" + # For Dependabots + - label: "github_actions" regexs: - - /.*/ + - /^chore*/ + - /^build(deps-dev)*/ events: [pull_request] targets: [title] + + # For Hacktoberfest only + # - label: 'hacktoberfest' + # regexs: + # - /.*/ + # events: [pull_request] + # targets: [title] diff --git a/.github/preview.png b/.github/preview.png new file mode 100644 index 00000000..cff6d95e Binary files /dev/null and b/.github/preview.png differ diff --git a/.github/release-drafter.yml b/.github/release-template.yml similarity index 97% rename from .github/release-drafter.yml rename to .github/release-template.yml index 8aff977b..03094f36 100644 --- a/.github/release-drafter.yml +++ b/.github/release-template.yml @@ -3,7 +3,7 @@ tag-template: 'v$RESOLVED_VERSION' categories: - title: 'πŸš€ Features' labels: - - 'πŸ’‘feature' + - 'feature' - 'enhancement' - title: 'πŸ› Bug Fixes' labels: diff --git a/.github/repository.config b/.github/repository.config new file mode 100644 index 00000000..b857bfa5 --- /dev/null +++ b/.github/repository.config @@ -0,0 +1,4 @@ +name=memories +username=warmachine028 +owner_name=warmachine028 +email=75939390+warmachine028@users.noreply.github.com diff --git a/.github/take-snapshot.mjs b/.github/take-snapshot.mjs new file mode 100644 index 00000000..31571603 --- /dev/null +++ b/.github/take-snapshot.mjs @@ -0,0 +1,16 @@ +import puppeteer from 'puppeteer' +const sleep = (ms) => new Promise((res) => setTimeout(res, ms)) + +const takeSnapShot = async () => { + // Replace with your actual deployed URL + const url = 'https://memories-canary.vercel.app' + const browser = await puppeteer.launch() + const page = await browser.newPage() + await page.setViewport({ width: 1920, height: 1080 }) + await page.goto(url) + await sleep(3000) + await page.screenshot({ path: '.github/preview.png' }) + await browser.close() +} + +takeSnapShot() diff --git a/.github/workflows/updateDate.sh b/.github/update-date.sh old mode 100755 new mode 100644 similarity index 87% rename from .github/workflows/updateDate.sh rename to .github/update-date.sh index edaa2dd6..4a46d2c8 --- a/.github/workflows/updateDate.sh +++ b/.github/update-date.sh @@ -10,11 +10,10 @@ DaySuffix() { } oldDate=`head -n 1 README.md` -newDate=`date "+ updated: %A, %d\`DaySuffix\` %B %Y"` +newDate=`date "+ updated on: %d\`DaySuffix\` %B %Y, %A"` lastLine='' sed -i "1s/.*/$newDate/" README.md echo Date Updated sed -i "$ d" README.md echo $lastLine >> README.md - diff --git a/.github/workflows/assign-author.yml b/.github/workflows/assign-author.yml index 876abd7e..aea525db 100644 --- a/.github/workflows/assign-author.yml +++ b/.github/workflows/assign-author.yml @@ -1,13 +1,13 @@ name: πŸ‘¨β€πŸ’» Assign Author on: - pull_request_target: - types: [opened, reopened] + pull_request_target: + types: [opened, reopened] jobs: - assign-author: - runs-on: ubuntu-latest - steps: - - uses: toshimaru/auto-author-assign@main - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + assign-author: + runs-on: ubuntu-latest + steps: + - uses: toshimaru/auto-author-assign@main + with: + repo-token: ${{ github.token }} diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml new file mode 100644 index 00000000..4cc36510 --- /dev/null +++ b/.github/workflows/check-pr-title.yml @@ -0,0 +1,43 @@ +name: πŸ“’ Check PR Titles + +on: + pull_request_target: + types: [opened, reopened, synchronize, edited, ready_for_review] + +jobs: + lint-pr-title: + name: Lint PR title + runs-on: ubuntu-latest + steps: + - name: Lint PR Title + uses: amannn/action-semantic-pull-request@v5 + if: ${{ !contains(fromJson('[ "dependabot[bot]", "dependabot-preview[bot]", "imgbot[bot]", "allcontributors" ]'), github.actor) }} + id: lint_pr_title + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject "{subject}" found in the pull request title "{title}" should start with a lowercase character. + + - name: Comment on PR + uses: marocchino/sticky-pull-request-comment@v2.9.0 + if: ${{ always() && steps.lint_pr_title.outputs.error_message != null && !contains(fromJson('[ "dependabot[bot]", "dependabot-preview[bot]", "imgbot[bot]", "allcontributors" ]'), github.actor)}} + with: + header: pr-title-lint-error + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + message: | + + We require all PRs to follow [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). + More details πŸ‘‡πŸΌ + ``` + ${{ steps.lint_pr_title.outputs.error_message}} + ``` + + - name: Delete comment + uses: marocchino/sticky-pull-request-comment@v2.9.0 + if: ${{ steps.lint_pr_title.outputs.error_message == null }} + with: + header: pr-title-lint-error + delete: true + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..edb42bdc --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: πŸ•΅οΈ CodeQL Analysis + +on: + workflow_dispatch: + push: + branches: ['main'] + pull_request: + # The branches below must be a subset of the branches above + branches: ['main'] + schedule: + - cron: '40 7 * * 2' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript'] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@main + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/first-contribution.yml b/.github/workflows/first-contribution.yml index 570abe85..4a1da3a8 100644 --- a/.github/workflows/first-contribution.yml +++ b/.github/workflows/first-contribution.yml @@ -1,28 +1,29 @@ name: πŸ™ Welcome First Time Contributors on: [pull_request_target, issues] + jobs: - greeting: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@main - if: ${{ github.event.sender.login != github.repository_owner }} - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: | - # Congratulations πŸŽ‰ @${{ github.actor }}, on creating your first issue in memories - - - Meanwhile if your liked this project, please make sure to star this 🌟🌟🌟. - - You can see my other projects too, which you might feel appealing. - - You also follow me for making more future contributions in open-source projects. + welcome: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@main + if: ${{ github.event.sender.login != github.repository_owner && !contains(fromJson('[ "dependabot[bot]", "dependabot-preview[bot]", "imgbot[bot]", "allcontributors" ]'), github.actor) }} + with: + repo-token: ${{ github.token }} + issue-message: | + # Congratulations πŸŽ‰ @${{ github.actor }}, on creating your first issue in ${{ github.event.repository.name }} + + - Meanwhile if your liked this project, please make sure to star this 🌟🌟🌟. + - You can see my other projects too, which you might feel appealing. + - You also follow me for making more future contributions in open-source projects. + + pr-message: | + # Congratulations πŸŽ‰ @${{ github.actor }}, on your first contribution in ${{ github.event.repository.name }} - pr-message: | - # Congratulations πŸŽ‰ @${{ github.actor }}, on your first contribution in memories - - - Make sure to star this project, if you liked contributing to it. 🌟🌟🌟 - - You can raise another new issue, if you find something is worth enhancement or needs a fix. - - You can see my other projects too, which you might feel appealing. - - You also follow me for making more future contributions in open-source projects. + - Make sure to star this project, if you liked contributing to it. 🌟🌟🌟 + - You can raise another new issue, if you find something is worth enhancement or needs a fix. + - You can see my other projects too, which you might feel appealing. + - You also follow me for making more future contributions in open-source projects. diff --git a/.github/workflows/greet-contributors.yml b/.github/workflows/greet-contributors.yml index f31235c0..1dfc4b46 100644 --- a/.github/workflows/greet-contributors.yml +++ b/.github/workflows/greet-contributors.yml @@ -1,30 +1,31 @@ -name: 🏑 Greet Contributor +name: 🏑 Greet Contributors on: - fork: - push: - branches: [main] - issues: - types: [opened] - pull_request_target: - types: [opened] - + fork: + issues: + types: [opened] + pull_request_target: + types: [opened] + jobs: - welcome: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@main - - uses: EddieHubCommunity/gh-action-community/src/welcome@main - if: ${{ github.event.sender.login != github.repository_owner }} - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: | - ### Hello @${{ github.actor }}, thank you for raising the issue. - - Thank you for raising an issue. Maintainer will soon investigate into the matter and get back to you as soon as possible. - Meanwhile feel free to support star the repo and share it with your friends. πŸ€“ πŸš€ - - pr-message: | - ### Hello @${{ github.actor }}, thank you for raising a pull request. - - Please make sure you have filled the PR template properly and followed our contributing guidelines. + greet: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@main + + - name: Comment on PR + uses: EddieHubCommunity/gh-action-community/src/welcome@main + if: ${{ github.event.sender.login != github.repository_owner && !contains(fromJson('[ "dependabot[bot]", "dependabot-preview[bot]", "imgbot[bot]", "allcontributors" ]'), github.actor) }} + with: + github-token: ${{ github.token }} + issue-message: | + ### Hello @${{ github.actor }}, thank you for raising the issue. + + Thank you for raising an issue. Maintainer will soon investigate into the matter and get back to you as soon as possible. + Meanwhile feel free to support star the repo and share it with your friends. πŸ€“ πŸš€ + + pr-message: | + ### Hello @${{ github.actor }}, thank you for raising a pull request. + + Please make sure you have filled the PR template properly and followed our contributing guidelines. diff --git a/.github/workflows/label-pr.yml b/.github/workflows/label-pr.yml index 2f32af7e..c0bb73e6 100644 --- a/.github/workflows/label-pr.yml +++ b/.github/workflows/label-pr.yml @@ -1,24 +1,18 @@ name: πŸ”– Add Labels to PRs -on: - pull_request_target: - types: - - opened - - reopened +on: pull_request_target jobs: - main: - runs-on: ubuntu-latest - - permissions: - contents: read - issues: write - pull-requests: write - - steps: - - name: Run PR Labeler - uses: hoho4190/issue-pr-labeler@v2.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - # disable-bot: false - config-file-name: labels.yml + main: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Run PR Labeler + uses: hoho4190/issue-pr-labeler@v2.0.0 + with: + token: ${{ github.token }} + disable-bot: false + config-file-name: labels.yml diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml deleted file mode 100644 index 4a248087..00000000 --- a/.github/workflows/pr-title.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: πŸ“’ Check PR Titles - -on: - pull_request_target: - types: [opened, reopened, synchronize, edited, ready_for_review] - -jobs: - lint-pr-title: - name: Lint PR title - runs-on: ubuntu-latest - steps: - - if: - ${{ !contains(fromJson('[ "dependabot[bot]", - "dependabot-preview[bot]", "allcontributors"]'), github.actor) }} - uses: amannn/action-semantic-pull-request@v5 #version 4.6.0 - id: lint_pr_title - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - subjectPattern: ^(?![A-Z]).+$ - subjectPatternError: | - The subject "{subject}" found in the pull request title "{title}" should start with a lowercase character. - - if: - ${{ always() && steps.lint_pr_title.outputs.error_message != null && - !contains(fromJson('[ "dependabot[bot]", "dependabot-preview[bot]", - "allcontributors"]'), github.actor)}} - name: Comment on PR - uses: marocchino/sticky-pull-request-comment@v2.9.0 - with: - header: pr-title-lint-error - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - message: | - - We require all PRs to follow [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/). - More details πŸ‘‡πŸΌ - ``` - ${{ steps.lint_pr_title.outputs.error_message}} - ``` - - if: ${{ steps.lint_pr_title.outputs.error_message == null }} - name: delete the comment - uses: marocchino/sticky-pull-request-comment@v2.9.0 - with: - header: pr-title-lint-error - delete: true - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index f68d9ba6..68f02840 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -1,42 +1,38 @@ name: πŸ€– Release Drafter on: - workflow_dispatch: - push: - # branches to consider in the event; optional, defaults to all - branches: - - main - # pull_request event is required only for autolabeler - pull_request: - # Only following types are handled by the action, but one can default to all as well - types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks - # pull_request_target: - # types: [opened, reopened, synchronize] + workflow_dispatch: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] permissions: - contents: read + contents: read jobs: - update_release_draft: - permissions: - # write permission is required to create a github release - contents: write - # write permission is required for autolabeler - # otherwise, read permission is required at least - pull-requests: write - runs-on: ubuntu-latest - steps: - # (Optional) GitHub Enterprise requires GHE_HOST variable set - #- name: Set GHE_HOST - # run: | - # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + update_release_draft: + permissions: + # write permission is required to create a github release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v6 - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml - # with: - # config-name: my-config.yml - # disable-autolabeler: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v6 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + with: + config-name: release-template.yml + # disable-autolabeler: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/update-date.yml b/.github/workflows/update-date.yml index 5f976376..2dbbe4a3 100644 --- a/.github/workflows/update-date.yml +++ b/.github/workflows/update-date.yml @@ -1,35 +1,30 @@ name: πŸ“† Update Date on: - workflow_dispatch: - push: - branches: [ main ] + workflow_dispatch: + push: + branches: [main] + +permissions: + contents: write jobs: - publish: - runs-on: ubuntu-latest - steps: - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} # You need to create your own token with commit rights - ref: ${{ github.ref }} # The branch you want to commit to - - - name: Give Permissions - run: | # if this line gives error, then run it locally - git update-index --chmod=+x ./.github/workflows/updateDate.sh + update-date: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@main + + - name: Give Permissions + run: git update-index --chmod=+x ./.github/update-date.sh # if this line gives error, then run it locally + + - name: Update README + run: .github/update-date.sh + shell: bash - - name: Update Files - run: ./.github/workflows/updateDate.sh - shell: bash - - - name: Push changes - uses: EndBug/add-and-commit@v9 - if: ${{ '' != '`tail -n 1 README.md`' }} - with: - committer_name: GitHub Actions - committer_email: actions@github.com - add: . - message: 'Updating Date' + - name: Push Changes + uses: ./.github/actions/push-changes + if: ${{ '' != '`tail -n 1 README.md`' }} + with: + message: 'feat: updated date in README.md' + diff --git a/.github/workflows/update-preview.yml b/.github/workflows/update-preview.yml new file mode 100644 index 00000000..acd0414a --- /dev/null +++ b/.github/workflows/update-preview.yml @@ -0,0 +1,38 @@ +name: πŸ“Έ Update Preview + +on: + workflow_dispatch: + push: + branches: [main] + # enable this for your repository if you want to trigger this workflow to observe specific file/directory changes + paths: + - 'client/**' + + +permissions: + contents: write + +jobs: + update-preview: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@main + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + + - name: Install Puppeteer + run: npm install puppeteer + + - name: Take snapshot + run: node .github/take-snapshot.mjs + + - name: Push Changes + uses: ./.github/actions/push-changes + with: + message: 'feat: updated preview in README.md' + paths: './.github/preview.png' + diff --git a/.gitignore b/.gitignore index f5ddbc2f..ccc08185 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build temp.js .idea .vercel +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 6988a23b..ffe70e7e 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ - updated: Monday, 09th September 2024 - + updated: Saturday, 19th October 2024
- Memories +

Cherishing the past with love

@@ -11,21 +10,13 @@
-# [Memories](https://memories-pritam.vercel.app) - -![line] - -## What's new? - -- Migration to Vite -- Migrate to react v18 +# [Memories](https://memories-latest.vercel.app) ![line] ## Table of Contents - [Introduction](#introduction) -- [Improvements](#improvements) - [Tech Stack Used](#tech-stack-used) - [Preview](#preview) - [Demo](#demo) @@ -37,32 +28,22 @@ ## Introduction -- In the past, individuals relied on traditional diaries to record their thoughts and experiences. -- However, as times have evolved, our requirements have remained constant. -- This WebApp aims to fulfill the need for a digital diary while enhancing the user experience. -- The anime film [Kimi no Na wa](https://en.wikipedia.org/wiki/Your_Name) has inspired me to continually enhance this project to its fullest potential. - -![line] - -## Improvements - -- [Improvements](./client/README.md) +- A modern, fast, and secure web app to store your memories. +- Built with the latest technologies and best practices. ![line] ## Tech Stack Used - Material UI: Styling & Icons -- MongoDB: For DataBase Management -- ExpressJs: For BackEnd Routing -- React: FrontEnd Developement -- NodeJS: For BackEnd developement -- Vercel: For hosting the frontEnd & backEnd production -- Vite: For FrontEnd Developement - -![Material UI](https://img.shields.io/badge/Material--UI-0081CB?style=for-the-badge&logo=material-ui&logoColor=white) ![Mongo DB](https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white) ![Express](https://img.shields.io/badge/Express.js-404D59?style=for-the-badge) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) -![Node JS](https://img.shields.io/badge/Node.js-43853D?style=for-the-badge&logo=node.js&logoColor=white) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) ![Redux](https://img.shields.io/badge/Redux-593D88?style=for-the-badge&logo=redux&logoColor=white) ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) -![JWT](https://img.shields.io/badge/json%20web%20tokens-323330?style=for-the-badge&logo=json-web-tokens&logoColor=pink) ![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white) ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) +- Postgres: Database +- Elysia: Backend Framework +- Bun: JavaScript Runtime +- Vercel: Frontend Hosting +- Render: Backend Hosting +- Clerk: Authentication + +![Material UI](https://img.shields.io/badge/Material--UI-0081CB?style=for-the-badge&logo=material-ui&logoColor=white) ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) ![React Router](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white) ![Redux](https://img.shields.io/badge/Redux-593D88?style=for-the-badge&logo=redux&logoColor=white) ![Bun](https://img.shields.io/badge/Bun-000000?style=for-the-badge&logo=bun&logoColor=e9ded2) ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) ![Vercel](https://img.shields.io/badge/Vercel-000000?style=for-the-badge&logo=vercel&logoColor=white) ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) ![Render](https://img.shields.io/badge/Render-000000?style=for-the-badge&logo=render&logoColor=white) ![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) ![Elysia](https://img.shields.io/badge/elysia-000000?style=for-the-badge&logo=elysia&logoColor=white) ![Neon DB](https://img.shields.io/badge/Neon%20DB-21946e?style=for-the-badge&logo=neon&logoColor=white) ![Prisma](https://img.shields.io/badge/Prisma-000000?style=for-the-badge&logo=prisma&logoColor=white) ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) ![YAML](https://img.shields.io/badge/clerk-%23ffffff.svg?style=for-the-badge&logo=clerk&logoColor=151515) ![line] diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. diff --git a/assets/demo.gif b/assets/demo.gif deleted file mode 100644 index c4c8b90d..00000000 Binary files a/assets/demo.gif and /dev/null differ diff --git a/client/.deepsource.toml b/client/.deepsource.toml new file mode 100644 index 00000000..40411339 --- /dev/null +++ b/client/.deepsource.toml @@ -0,0 +1,24 @@ +# DeepSource configuration file +# Learn how to configure: https://docs.deepsource.com/docs/configuration +version = 1 + +test_patterns = ["*/test/**"] + +exclude_patterns = [ + "public/**", + "dist/**", + "node_modules/**" +] + +[[analyzers]] +name = "shell" +enabled = true + +[[analyzers]] +name = "javascript" +enabled = true + + [analyzers.meta] + plugins = ["react"] + environment = ["browser"] + module_system = "es-modules" diff --git a/client/.env.example b/client/.env.example index d3dcb4bf..2f54100d 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1 +1,2 @@ -VITE_GOOGLE_CLIENT_ID = \ No newline at end of file +VITE_CLERK_PUBLISHABLE_KEY= +VITE_API_URL= \ No newline at end of file diff --git a/client/.eslintrc.js b/client/.eslintrc.js deleted file mode 100644 index 26409b12..00000000 --- a/client/.eslintrc.js +++ /dev/null @@ -1,49 +0,0 @@ -module.export = { - env: { es6: true, node: true, browser: true }, - extends: ['eslint:recommended', 'plugin:react/recommended', 'airbnb'], - globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly' }, - parser: '@babel/eslint-parser', - parserOptions: { - ecmaFeatures: { jsx: true }, - ecmaVersion: 'latest', - requireConfigFile: false, - sourceType: 'module', - babelOptions: { - presets: ['@babel/preset-react'], - }, - }, - plugins: ['react'], - rules: { - quotes: [0, 'single'], - indent: ['error', 4], - 'import/extensions': 0, - 'react/prop-types': 0, - 'linebreak-style': 0, - 'react/state-in-constructor': 0, - 'import/prefer-default-export': 0, - 'max-len': [2, 250], - 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }], - 'no-underscore-dangle': ['error', { allow: ['_d', '_dh', '_h', '_id', '_m', '_n', '_t', '_text'] }], - 'object-curly-newline': 0, - 'react/jsx-filename-extension': 0, - 'react/jsx-one-expression-per-line': 0, - 'jsx-a11y/click-events-have-key-events': 0, - 'jsx-a11y/alt-text': 0, - 'jsx-a11y/no-autofocus': 0, - 'jsx-a11y/no-static-element-interactions': 0, - 'react/no-array-index-key': 0, - 'jsx-a11y/anchor-is-valid': [ - 'error', - { - components: ['Link'], - specialLink: ['to', 'hrefLeft', 'hrefRight'], - aspects: ['noHref', 'invalidHref', 'preferButton'], - }, - ], - }, - settings: { - react: { - version: 'latest', // "detect" automatically picks the version you have installed. - }, - }, -} diff --git a/client/README.md b/client/README.md deleted file mode 100644 index 0597f68f..00000000 --- a/client/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# FEATURES - -## Additional Improvements - -- Glass finish Card Post Components. -- CRUD based Operations, Post Search Functionality with Tags -- Details Page of each Post Card, Recommended Posts. -- Image Compression (compresses every image under 1MB) -- Like - Comment - Tag functionality and 2 Way Authentication (JWT Token & Google OAuth) -- Randomised Custom User Avatar Images. -- Image Drag and Drop functionality in Preview while creation -- Private Post and Comment Deletion Functionality -- Attention to detail features like custom Private button and LinearProgress. -- Post Owners and commenters can regulate comments in their posts. -- Comments section only visible if at least 1 comment exist in a post. -- New User-Detail page including newly written dataBase query and Backend Logic. -- Addition of Posts Liked by user Component. -- Customised comment containers with User avatar and post times. -- Clickable Chips and Custom Tabs in UserDetails page. -- Memories is now a Progressive Web App πŸŽ‰πŸŽ‰πŸŽŠπŸŽŠ. -- Added Credential Update Feature for users. -- Remember Me and Forgot Password in Authentication using NodeMailer πŸ’•πŸ’• -- Improved UI By adding Hover animations on post cards and Media πŸ–ΌοΈπŸŒŸ -- New Snackbar Alerts from Material UI replacing Basic browser Alerts. -- Instant comment Actions and Post Updation. -- Added Scroll To Top Floating Action Button and Floating NavBar -- Added redirects on post not found -- Added Circular Progress in Form Submit button while Creating a post -- User Avatar in Posts -- Post Thumbnails -- Delete Confirmation Dialog box -- Theme Toggle by **[Avinash Dubey](https://github.com/avinasdube)** -- Migrate to Vite by **[@Pranav Ajay](https://github.com/cyblogerz)** \ No newline at end of file diff --git a/client/bun.lockb b/client/bun.lockb new file mode 100644 index 00000000..2b8ed96a Binary files /dev/null and b/client/bun.lockb differ diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 00000000..37a0cf81 --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,42 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import eslintConfigPrettier from 'eslint-config-prettier' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2025, + globals: { + ...globals.browser, + __dirname: true + }, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module' + } + }, + settings: { react: { version: '19.0' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'no-unused-vars': ['warn'], + 'react/jsx-no-target-blank': 'off', + 'react/prop-types': 'off', + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }] + } + }, + eslintConfigPrettier +] diff --git a/client/index.html b/client/index.html index 21a38b6c..f1761f00 100644 --- a/client/index.html +++ b/client/index.html @@ -1,44 +1,52 @@ - - + + + + + + Memories | Your Social Media App + + + - - - - - - - - + + + - - - - - - + + - - - - - + + + + + + + + + + - - - - - - - - - - Memories - - - - -
- - + + + + + + + + + + + +
+ + diff --git a/client/jsconfig.json b/client/jsconfig.json new file mode 100644 index 00000000..37d90e94 --- /dev/null +++ b/client/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/client/package-lock.json b/client/package-lock.json deleted file mode 100644 index 9e2b2e65..00000000 --- a/client/package-lock.json +++ /dev/null @@ -1,3810 +0,0 @@ -{ - "name": "memories-client", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "memories-client", - "version": "1.0.0", - "dependencies": { - "@emotion/react": "^11.13.3", - "@emotion/styled": "^11.13.0", - "@mui/icons-material": "^5.16.7", - "@mui/material": "^5.16.7", - "@react-oauth/google": "^0.12.1", - "avataaars2": "^1.1.1", - "axios": "^1.7.7", - "browser-image-compression": "^2.0.2", - "jwt-decode": "^4.0.0", - "lodash": "^4.17.21", - "moment": "^2.30.1", - "mui-chips-input": "^2.1.5", - "react": "^18.2.0", - "react-dom": "^18.3.1", - "react-redux": "^9.1.2", - "react-router-dom": "^6.26.1", - "react-swipeable-views-v18": "^1.1.27", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "vite": "^5.4.3" - }, - "devDependencies": { - "@babel/eslint-parser": "^7.25.1", - "@babel/preset-react": "^7.24.7", - "@vitejs/plugin-react": "^4.3.1", - "eslint": "^9.10.0", - "eslint-plugin-require": "^0.0.1" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/eslint-parser": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.1.tgz", - "integrity": "sha512-Y956ghgTT4j7rKesabkh5WeqgSFZVFwaPR0IWFm7KFHFmmJ4afbG49SmfW4S+GyRPx0Dy5jxEWA5t0rpxfElWg==", - "dev": true, - "dependencies": { - "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", - "eslint-visitor-keys": "^2.1.0", - "semver": "^6.3.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || >=14.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0", - "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" - } - }, - "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==", - "dependencies": { - "@babel/types": "^7.25.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", - "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", - "dependencies": { - "@babel/types": "^7.25.2" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", - "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", - "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.2.tgz", - "integrity": "sha512-KQsqEAVBpU82NM/B/N9j9WOdphom1SZH3R+2V7INrQUH+V9EBFwZsEJl8eBIVeQE62FxJCc70jzEZwqU7RcVqA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/plugin-syntax-jsx": "^7.24.7", - "@babel/types": "^7.25.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", - "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", - "dev": true, - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", - "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", - "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", - "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", - "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-transform-react-display-name": "^7.24.7", - "@babel/plugin-transform-react-jsx": "^7.24.7", - "@babel/plugin-transform-react-jsx-development": "^7.24.7", - "@babel/plugin-transform-react-pure-annotations": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/parser": "^7.25.3", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.2", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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==", - "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@emotion/babel-plugin": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.12.0.tgz", - "integrity": "sha512-y2WQb+oP8Jqvvclh8Q55gLUyb7UFvgv7eJfsj7td5TToBrIUtPay2kMrZi4xjq9qw2vD0ZR5fSho0yqoFgX7Rw==", - "dependencies": { - "@babel/helper-module-imports": "^7.16.7", - "@babel/runtime": "^7.18.3", - "@emotion/hash": "^0.9.2", - "@emotion/memoize": "^0.9.0", - "@emotion/serialize": "^1.2.0", - "babel-plugin-macros": "^3.1.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^4.0.0", - "find-root": "^1.1.0", - "source-map": "^0.5.7", - "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", - "integrity": "sha512-iqouYkuEblRcXmylXIwwOodiEK5Ifl7JcX7o6V4jI3iW4mLXX3dmt5xwBtIkJiQEXFAI+pC8X0i67yiPkH9Ucw==", - "dependencies": { - "@emotion/memoize": "^0.9.0", - "@emotion/sheet": "^1.4.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "stylis": "4.2.0" - } - }, - "node_modules/@emotion/hash": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", - "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==" - }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.0.tgz", - "integrity": "sha512-SHetuSLvJDzuNbOdtPVbq6yMMMlLoW5Q94uDqJZqy50gcmAjxFkVqmzqSGEFq9gT2iMuIeKV1PXVWmvUhuZLlQ==", - "dependencies": { - "@emotion/memoize": "^0.9.0" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", - "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==" - }, - "node_modules/@emotion/react": { - "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.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0", - "@emotion/weak-memoize": "^0.4.0", - "hoist-non-react-statics": "^3.3.1" - }, - "peerDependencies": { - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/serialize": { - "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.10.0", - "@emotion/utils": "^1.4.0", - "csstype": "^3.0.2" - } - }, - "node_modules/@emotion/sheet": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", - "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==" - }, - "node_modules/@emotion/styled": { - "version": "11.13.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.0.tgz", - "integrity": "sha512-tkzkY7nQhW/zC4hztlwucpT8QEZ6eUzpXDRhww/Eej4tFfO0FxQYWRyg/c5CCXa4d/f174kqeXYjuQRnhzf6dA==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.12.0", - "@emotion/is-prop-valid": "^1.3.0", - "@emotion/serialize": "^1.3.0", - "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", - "@emotion/utils": "^1.4.0" - }, - "peerDependencies": { - "@emotion/react": "^11.0.0-rc.0", - "react": ">=16.8.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@emotion/unitless": { - "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", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", - "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", - "peerDependencies": { - "react": ">=16.8.0" - } - }, - "node_modules/@emotion/utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.0.tgz", - "integrity": "sha512-spEnrA1b6hDR/C68lC2M7m6ALPUHZC0lIY7jAS/B/9DuuO1ZP04eov8SMv/6fwRd8pzmsn2AuJEznRREWlQrlQ==" - }, - "node_modules/@emotion/weak-memoize": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", - "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", - "dev": true, - "dependencies": { - "@eslint/object-schema": "^2.1.4", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", - "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", - "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", - "dev": true, - "dependencies": { - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", - "dev": true, - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@mui/core-downloads-tracker": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.16.7.tgz", - "integrity": "sha512-RtsCt4Geed2/v74sbihWzzRs+HsIQCfclHeORh5Ynu2fS4icIKozcSubwuG7vtzq2uW3fOR1zITSP84TNt2GoQ==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - } - }, - "node_modules/@mui/icons-material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.16.7.tgz", - "integrity": "sha512-UrGwDJCXEszbDI7yV047BYU5A28eGJ79keTCP4cc74WyncuVrnurlmIRxaHL8YK+LI1Kzq+/JM52IAkNnv4u+Q==", - "dependencies": { - "@babel/runtime": "^7.23.9" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@mui/material": "^5.0.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/material": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.16.7.tgz", - "integrity": "sha512-cwwVQxBhK60OIOqZOVLFt55t01zmarKJiJUWbk0+8s/Ix5IaUzAShqlJchxsIQ4mSrWqgcKCCXKtIlG5H+/Jmg==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/core-downloads-tracker": "^5.16.7", - "@mui/system": "^5.16.7", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", - "@popperjs/core": "^2.11.8", - "@types/react-transition-group": "^4.4.10", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1", - "react-is": "^18.3.1", - "react-transition-group": "^4.4.5" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0", - "react-dom": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/private-theming": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.16.6.tgz", - "integrity": "sha512-rAk+Rh8Clg7Cd7shZhyt2HGTTE5wYKNSJ5sspf28Fqm/PZ69Er9o6KX25g03/FG2dfpg5GCwZh/xOojiTfm3hw==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.16.6", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/styled-engine": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.16.6.tgz", - "integrity": "sha512-zaThmS67ZmtHSWToTiHslbI8jwrmITcN93LQaR2lKArbvS7Z3iLkwRoiikNWutx9MBs8Q6okKvbZq1RQYB3v7g==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@emotion/cache": "^11.11.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.4.1", - "@emotion/styled": "^11.3.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, - "node_modules/@mui/system": { - "version": "5.16.7", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.16.7.tgz", - "integrity": "sha512-Jncvs/r/d/itkxh7O7opOunTqbbSSzMTHzZkNLM+FjAOg+cYAZHrPDlYe1ZGKUYORwwb2XexlWnpZp0kZ4AHuA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.16.6", - "@mui/styled-engine": "^5.16.6", - "@mui/types": "^7.2.15", - "@mui/utils": "^5.16.6", - "clsx": "^2.1.0", - "csstype": "^3.1.3", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/types": { - "version": "7.2.15", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.15.tgz", - "integrity": "sha512-nbo7yPhtKJkdf9kcVOF8JZHPZTmqXjJ/tI0bdWgHg5tp9AnIN4Y7f7wm9T+0SyGYJk76+GYZ8Q5XaTYAsUHN0Q==", - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@mui/utils": { - "version": "5.16.6", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz", - "integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@mui/types": "^7.2.15", - "@types/prop-types": "^15.7.12", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "react-is": "^18.3.1" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18.0.0", - "react": "^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "dependencies": { - "eslint-scope": "5.1.1" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@react-oauth/google": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", - "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@remix-run/router": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", - "integrity": "sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", - "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz", - "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz", - "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz", - "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz", - "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz", - "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz", - "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz", - "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz", - "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz", - "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz", - "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz", - "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz", - "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz", - "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.6.8", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.20.6", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", - "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.7" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" - }, - "node_modules/@types/node": { - "version": "20.8.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.9.tgz", - "integrity": "sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg==", - "peer": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" - }, - "node_modules/@types/react": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", - "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-transition-group": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", - "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", - "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", - "dev": true, - "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.14.2" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0" - } - }, - "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/avataaars2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/avataaars2/-/avataaars2-1.1.1.tgz", - "integrity": "sha512-257kayHiNXsqJGI6yfUh1qNOPhq0rGF4SkgPvyLBzrQv6L4wOwtm3Xbq4OdJJU+VFzKOB4lY+hEhQueaA8xPhg==", - "dependencies": { - "lodash": ">=4.17.21", - "prop-types": "^15.7.2", - "react": "17.0.0", - "ts-node": "^10.9.1" - }, - "peerDependencies": { - "react": "^17.0.0" - } - }, - "node_modules/avataaars2/node_modules/react": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.0.tgz", - "integrity": "sha512-rG9bqS3LMuetoSUKHN8G3fMNuQOePKDThK6+2yXFWtoeTDLVNh/QCaxT+Jr+rNf4lwNXpx+atdn3Aa0oi8/6eQ==", - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "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, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browser-image-compression": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", - "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", - "dependencies": { - "uzip": "0.20201231.0" - } - }, - "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "engines": { - "node": ">=6" - } - }, - "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==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "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 - }, - "node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", - "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", - "dev": true - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "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, - "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==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint": { - "version": "9.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", - "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.10.0", - "@eslint/plugin-kit": "^0.1.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.0", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.0.2", - "eslint-visitor-keys": "^4.0.0", - "espree": "^10.1.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "json-stable-stringify-without-jsonify": "^1.0.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" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-require": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-require/-/eslint-plugin-require-0.0.1.tgz", - "integrity": "sha512-vlGkIKzY70RAVMglfrnZtC9fe/wxMakhNEHUkV6Ye1dB28YNR9mGNeWtaIWcN45vmuez7zrocJOMsylGYDBKrg==", - "dev": true, - "engines": { - "node": ">=0.10" - }, - "peerDependencies": { - "eslint": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "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": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", - "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "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/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hoist-non-react-statics/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" - }, - "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "engines": { - "node": ">=18" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "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==" - }, - "node_modules/mui-chips-input": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/mui-chips-input/-/mui-chips-input-2.1.5.tgz", - "integrity": "sha512-A3kuSbGKv6avDFdMzb7sax7PaSAC2de8WCliKdxph0ajsPlB/x/tH5mO9XlFFAPR0D30KceAJssCZx3z+5nE0Q==", - "peerDependencies": { - "@emotion/react": "^11.5.0", - "@emotion/styled": "^11.3.0", - "@mui/icons-material": "^5.0.0", - "@mui/material": "^5.0.0", - "@types/react": "^18.0.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" - }, - "node_modules/postcss": { - "version": "8.4.44", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz", - "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "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/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", - "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-router": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.1.tgz", - "integrity": "sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ==", - "dependencies": { - "@remix-run/router": "1.19.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/react-router-dom": { - "version": "6.26.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.1.tgz", - "integrity": "sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw==", - "dependencies": { - "@remix-run/router": "1.19.1", - "react-router": "6.26.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/react-swipeable-views-v18": { - "version": "1.1.27", - "resolved": "https://registry.npmjs.org/react-swipeable-views-v18/-/react-swipeable-views-v18-1.1.27.tgz", - "integrity": "sha512-EtfVa+Gl2qwNq/WE0M/sKdcSgo7P1A6f22LVCxty/2LA4BVu/Tl53FFniVylqGp7T+uNnHBuxeXTzsKn89fJTg==", - "dependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "^5.1.3" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" - }, - "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", - "dependencies": { - "@types/estree": "1.0.5" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-ansi": { - "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" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/stylis": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", - "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "peer": true - }, - "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/uzip": { - "version": "0.20201231.0", - "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", - "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==" - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" - }, - "node_modules/vite": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz", - "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==", - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/client/package.json b/client/package.json index 1ad9be49..a673265f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,61 +1,65 @@ { - "name": "memories-client", - "author": "warmachine028 ", - "version": "1.0.0", - "description": "This is the client side for memories Project", - "private": true, - "proxy": "http://localhost:5000", + "name": "memories", "scripts": { - "dev": "vite", - "build": "vite build", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "dev": "bun x vite --host", + "build": "bun x vite build", + "lint": "eslint .", + "preview": "bun x vite preview", + "start": "bun x vite preview" + }, + "type": "module", + "version": "1.0.0", + "author": { + "email": "pritamkundu771@gmail.com", + "name": "Pritam Kundu", + "url": "https://github.com/warmachine028" + }, + "description": "A modern, fast, and secure web app to store your memories built with the latest technologies and best practices.", + "license": "MIT", + "homepage": "https://memories-latest.vercel.app", + "repository": { + "type": "git", + "url": "https://github.com/warmachine028/memories" }, + "bugs": { + "url": "https://github.com/warmachine028/memories/issues" + }, + "keywords": [ + "social-media", + "social", + "memories", + "diary", + "digital-diary" + ], "dependencies": { + "@clerk/clerk-react": "^5.13.1", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@mui/icons-material": "^5.16.7", - "@mui/material": "^5.16.7", - "@react-oauth/google": "^0.12.1", - "avataaars2": "^1.1.1", + "@mui/icons-material": "6.1.5", + "@mui/lab": "^6.0.0-beta.12", + "@mui/material": "6.1.5", + "@tanstack/react-query": "^5.59.16", + "@tanstack/react-query-devtools": "^5.59.16", "axios": "^1.7.7", - "browser-image-compression": "^2.0.2", - "jwt-decode": "^4.0.0", - "lodash": "^4.17.21", "moment": "^2.30.1", - "mui-chips-input": "^2.1.5", - "react": "^18.2.0", - "react-dom": "^18.3.1", - "react-redux": "^9.1.2", - "react-router-dom": "^6.26.1", - "react-swipeable-views-v18": "^1.1.27", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "vite": "^5.4.3" + "react": "^19.0.0-rc-fb9a90fa48-20240614", + "react-dom": "^19.0.0-rc-fb9a90fa48-20240614", + "react-intersection-observer": "^9.13.1", + "react-router-dom": "^6.27.0", + "zustand": "^5.0.0" }, "devDependencies": { - "@babel/eslint-parser": "^7.25.1", - "@babel/preset-react": "^7.24.7", - "@vitejs/plugin-react": "^4.3.1", - "eslint": "^9.10.0", - "eslint-plugin-require": "^0.0.1" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "@eslint/js": "^9.13.0", + "@types/eslint-config-prettier": "^6.11.3", + "@types/eslint__js": "^8.42.3", + "@vitejs/plugin-react-swc": "^3.7.1", + "eslint": "^9.13.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0-rc-fb9a90fa48-20240614", + "eslint-plugin-react-refresh": "0.4.14", + "globals": "^15.11.0", + "prettier": "^3.3.3", + "vite": "^6.0.0-beta.5" } -} +} \ No newline at end of file diff --git a/client/prettier.config.mjs b/client/prettier.config.mjs new file mode 100644 index 00000000..4e972882 --- /dev/null +++ b/client/prettier.config.mjs @@ -0,0 +1,19 @@ +// prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs + +/** + * @see https://prettier.io/docs/en/configuration.html + * @type {import("prettier").Config} + */ +const config = { + $schema: 'http://json.schemastore.org/prettierrc', + bracketSpacing: true, + printWidth: 500, + proseWrap: 'always', + semi: false, + singleQuote: true, + tabWidth: 4, + trailingComma: 'none', + useTabs: true +} + +export default config diff --git a/client/public/_redirects b/client/public/_redirects deleted file mode 100644 index f8243379..00000000 --- a/client/public/_redirects +++ /dev/null @@ -1 +0,0 @@ -/* /index.html 200 \ No newline at end of file diff --git a/client/public/logo.png b/client/public/favicon.ico similarity index 100% rename from client/public/logo.png rename to client/public/favicon.ico diff --git a/client/public/manifest.json b/client/public/manifest.json deleted file mode 100644 index 4d99349c..00000000 --- a/client/public/manifest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "short_name": "memories", - "name": "Memories", - "description": "A memo website to store your memories. Inspired by Your Name", - "icons": [ - { - "src": "logo.png", - "sizes": "1024x1024", - "type": "image/png", - "purpose": "any maskable" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#ffffff", - "background_color": "#000000" -} diff --git a/client/public/maskable.png b/client/public/maskable.png deleted file mode 100644 index d694e0ea..00000000 Binary files a/client/public/maskable.png and /dev/null differ diff --git a/client/public/robots.txt b/client/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/client/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 00000000..0b437a10 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,38 @@ +import { AppRouter, Navbar, ScrollToTop, Snackbar } from '@/components' +import { ClerkProvider } from '@clerk/clerk-react' +import { BrowserRouter } from 'react-router-dom' +import { ThemeProvider } from '@/providers' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + +const MemoriesApp = () => { + return ( + + + + + + + ) +} + +const App = () => { + const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY + if (!PUBLISHABLE_KEY) { + throw new Error('Missing Clerk Publishable Key') + } + const queryClient = new QueryClient() + + return ( + + + + + + + + + ) +} + +export default App diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx deleted file mode 100644 index c023b1fb..00000000 --- a/client/src/App/App.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useContext, useState } from 'react' -import PostDetails from '../components/PostDetails/PostDetails' -import Navbar from '../components/Navbar/Navbar' -import FloatingNavbar from '../components/Navbar/FloatingNavbar' -import Home from '../components/Home' -import Auth from '../components/Auth/Auth' -import UserDetails, { PublicProfile } from '../components/User/Details' -import UserUpdate from '../components/User/Update' -import SnackBar from '../components/SnackBar' -import ScrollToTop from '../components/ScrollToTop' -import ForgotPassword from '../components/Auth/ForgotPassword' -import { GoogleOAuthProvider } from '@react-oauth/google' -import { SnackbarContext } from '../contexts/SnackbarContext' -import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom' -import { classes, Root } from './styles' -import { ModeContext, modes } from '../contexts/ModeContext' - -const App = () => { - const [user, setUser] = useState(JSON.parse(localStorage.getItem('profile'))) - const { snackBarProps } = useContext(SnackbarContext) - - const [mode, setMode] = useState(modes.light) - - // FUNCTION TO TOGGLE THEMES ON CLICK - - const modeToggle = () => { - mode === modes.light ? setMode(modes.dark) : setMode(modes.light) - } - - return ( - - - - -
- - - - } /> - } /> - } /> - } /> - : } /> - } /> - : } /> - : } /> - : } /> - : } /> - - - -
-
-
-
-
- ) -} - -export default App diff --git a/client/src/App/styles.js b/client/src/App/styles.js deleted file mode 100644 index f5f8f819..00000000 --- a/client/src/App/styles.js +++ /dev/null @@ -1,70 +0,0 @@ -import { styled } from '@mui/material/styles' -import Image from '../images/background.jpg' - -const PREFIX = 'App' -export const classes = { - rootLight: `${PREFIX}-rootLight`, - rootDark: `${PREFIX}-rootDark`, - floatingNavbar: `${PREFIX}-floatingNavbar`, - container: `${PREFIX}-container`, - blur: `${PREFIX}-blur`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.rootLight}`]: { - backgroundImage: `url(${Image})`, - // backgroundImage: `url(https://source.unsplash.com/1920x1080/?dark,night,technology)`, - bacgroundSize: 'contain', - backgroundPosition: 'center', - backgroundAttachment: 'fixed', - maxWidth: '100%', - maxHeight: '100%', - minHeight: '1200px', - padding: '10px 5px', - [theme.breakpoints.down(948)]: { - backgroundPositionY: 'center', - minHeight: '1080px', - }, - }, - [`&.${classes.rootDark}`]: { - backgroundImage: `url(${Image})`, - // backgroundImage: `url(https://source.unsplash.com/1920x1080/?dark,night,technology)`, - bacgroundSize: 'contain', - backgroundPosition: 'center', - backgroundAttachment: 'fixed', - maxWidth: '100%', - maxHeight: '100%', - minHeight: '1200px', - padding: '10px 5px', - [theme.breakpoints.down(948)]: { - backgroundPositionY: 'center', - minHeight: '1080px', - }, - }, - [`& .${classes.blur}`]: { - minHeight: '1200px', - [theme.breakpoints.down(948)]: { - backgroundPositionY: 'center', - minHeight: '100%', - }, - }, - [`& .${classes.floatingNavbar}`]: { - borderRadius: '5px', - margin: '0 16px 0 6px', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - // padding: '10px 50px', - backgroundColor: 'rgba(255, 255, 255, .09)', //"transparent" //"rgba(69, 114, 200)" - backdropFilter: 'blur(10px)', - [theme.breakpoints.down('md')]: { - flexDirection: 'column', - }, - [theme.breakpoints.down(360)]: { - padding: '10px 30px', - }, - }, -})) - -export default Root diff --git a/client/src/actions/auth.js b/client/src/actions/auth.js deleted file mode 100644 index db8d1e9b..00000000 --- a/client/src/actions/auth.js +++ /dev/null @@ -1,62 +0,0 @@ -import { AUTH } from '../constants/actionTypes' -import * as api from '../api' - -export const signin = (formData, history, snackBar) => async (dispatch) => { - try { - // log in the user ... - const { data } = await api.signIn(formData) - dispatch({ type: AUTH, data }) - snackBar('success', 'Logged in Successfully') - history('/') - } catch (error) { - snackBar('error', error.response.data.message) - } -} - -export const googleSignIn = (formData, history, snackBar) => async (dispatch) => { - try { - await api.googleSignIn(formData.result) - dispatch({ type: AUTH, data: formData }) - history('/') - snackBar('success', 'Logged in Successfully') - } catch (error) { - snackBar('error', error.response.data.message) - } -} - -export const signup = (formData, history, snackBar) => async (dispatch) => { - try { - // sign up the user ... - const { data } = await api.signUp(formData) - dispatch({ type: AUTH, data }) - snackBar('success', 'Registration Successful ! Welcome to memories') - history('/') - } catch (error) { - snackBar('error', error.response.data.message) - } -} - -export const forgotPassword = (formData, history, snackBar, setLoading) => async () => { - try { - await api.sendResetLink(formData) - snackBar('success', 'Reset Link sent to your Email. Now Reset Password') - setLoading(false) - history('/') - } catch (error) { - snackBar('error', error.response.data.message) - console.log(`error: ${error.response.data.error}`) - } - setLoading(false) -} - -export const setNewPassword = (formData, history, snackBar, setLoading) => async () => { - try { - await api.setNewPassword(formData) - snackBar('success', 'Password was successfully reset. Now Log in') - setLoading(false) - history('/') - } catch (error) { - snackBar('error', error.response.data.message) - } - setLoading(false) -} diff --git a/client/src/actions/comments.js b/client/src/actions/comments.js deleted file mode 100644 index 427d7fe2..00000000 --- a/client/src/actions/comments.js +++ /dev/null @@ -1,51 +0,0 @@ -import { CREATE_COMMENT, DELETE_COMMENT, FETCHED_COMMENTS, FETCHING_COMMENTS, FETCH_COMMENTS } from '../constants/actionTypes' -import * as api from '../api' - -export const getComments = (postId, snackBar) => async (dispatch) => { - try { - dispatch({ type: FETCHING_COMMENTS }) - const { data } = await api.fetchComments(postId) - dispatch({ type: FETCH_COMMENTS, payload: data }) - dispatch({ type: FETCHED_COMMENTS }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: FETCHED_COMMENTS }) - } else { - snackBar('error', error) - } - } -} - -export const createComment = (comment, snackBar) => async (dispatch) => { - try { - dispatch({ type: FETCHING_COMMENTS }) - const { data } = await api.createComment(comment) - dispatch({ type: CREATE_COMMENT, payload: data }) - dispatch({ type: FETCHED_COMMENTS }) - return data - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: FETCHED_COMMENTS }) - } else { - snackBar('error', error) - } - } -} - -export const deleteComment = (commentId, snackBar) => async (dispatch) => { - try { - dispatch({ type: FETCHING_COMMENTS }) - await api.deleteComment(commentId) - dispatch({ type: DELETE_COMMENT, payload: commentId }) - dispatch({ type: FETCHED_COMMENTS }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: FETCHED_COMMENTS }) - } else { - snackBar('error', error) - } - } -} diff --git a/client/src/actions/posts.js b/client/src/actions/posts.js deleted file mode 100644 index 6fb28801..00000000 --- a/client/src/actions/posts.js +++ /dev/null @@ -1,187 +0,0 @@ -import { - // - FETCHING_RECOMMENDED_POSTS, - FETCH_RECOMMENDED, - FETCHED_RECOMMENDED_POSTS, - FETCH_ALL, - FETCH_BY_SEARCH, - USER_DETAILS, - CREATE, - UPDATE, - DELETE, - START_LOADING, - END_LOADING, - FETCH_POST, - CREATED_POST, - CREATING_POST, - DELETING_POST, - DELETED_POST, - FETCHING_COMMENTS, - FETCHED_COMMENTS, - FETCH_COMMENTS, -} from '../constants/actionTypes' -import * as api from '../api' - -const sanitize = ({ tags, search }) => { - return { - tags: tags.replace(/#/g, '%23').replace(/ /g, '%20'), - search: search?.replace(/#/g, '%23').replace(/ /g, '%20'), - } -} -// Action Creators -export const getPosts = (page, snackBar) => async (dispatch) => { - try { - dispatch({ type: START_LOADING }) - const { - data: { data, curretPage: currentPage, numberOfPages }, - } = await api.fetchPosts(page) - dispatch({ type: FETCH_ALL, payload: { data, currentPage: currentPage, numberOfPages } }) - dispatch({ type: END_LOADING }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please refresh after some time`) - dispatch({ type: END_LOADING }) - } else { - snackBar('error', error) - } - } -} - -export const getUserDetails = (userId, snackBar) => async (dispatch) => { - try { - dispatch({ type: START_LOADING }) - const { data } = await api.userDetails(userId) - dispatch({ type: USER_DETAILS, payload: { data: data } }) - dispatch({ type: END_LOADING }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: END_LOADING }) - } else { - snackBar('error', error) - } - } -} - -export const getUserPostsByType = (userId, page, type) => async (dispatch) => { - const upperType = type.toUpperCase() - const fetchingType = `FETCHING_${upperType}_POSTS` - const fetchType = `FETCH_${upperType}` - const fetchedType = `FETCHED_${upperType}_POSTS` - - try { - dispatch({ type: fetchingType }) - const { - data: { data, numberOfPages }, - } = await api.fetchUserPostsByType(userId, page, type) - dispatch({ type: fetchType, payload: { data, numberOfPages } }) - dispatch({ type: fetchedType }) - } catch (error) { - console.log(error) - } -} - -export const getUserComments = (userId, page) => async (dispatch) => { - try { - dispatch({ type: FETCHING_COMMENTS }) - const { - data: { data, numberOfPages }, - } = await api.fetchUserComments(userId, page) - - dispatch({ type: FETCH_COMMENTS, payload: { comments: data, numberOfPages } }) - dispatch({ type: FETCHED_COMMENTS }) - } catch (error) { - console.log(error) - } -} - -export const getPost = (id, history, snackBar) => async (dispatch) => { - try { - dispatch({ type: START_LOADING }) - const { data } = await api.fetchPost(id) - dispatch({ type: FETCH_POST, payload: { post: data } }) - dispatch({ type: END_LOADING }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: END_LOADING }) - } else { - snackBar('error', error.response.data) - history('/') - } - } -} - -export const getPostsBySearch = (searchQuery) => async (dispatch) => { - searchQuery = sanitize(searchQuery) - try { - dispatch({ type: START_LOADING }) - const { - data: { data }, - } = await api.fetchPostsBySearch(searchQuery) - dispatch({ type: FETCH_BY_SEARCH, payload: { data } }) - dispatch({ type: END_LOADING }) - } catch (error) { - console.log(error) - } -} - -export const getRecommendedPosts = (tags) => async (dispatch) => { - tags = sanitize({ tags }).tags - try { - dispatch({ type: FETCHING_RECOMMENDED_POSTS }) - const { - data: { data }, - } = await api.fetchPostsBySearch({ tags: tags }) - dispatch({ type: FETCH_RECOMMENDED, payload: { data } }) - dispatch({ type: FETCHED_RECOMMENDED_POSTS }) - } catch (error) { - console.log(error) - } -} - -export const createPost = (post, history, snackBar, callBack) => async (dispatch) => { - try { - dispatch({ type: CREATING_POST }) - const { data } = await api.createPost(post) - dispatch({ type: CREATE, payload: data }) - history(`/posts/${data._id}`) - snackBar('success', 'Post created successfully') - dispatch({ type: CREATED_POST }) - } catch (error) { - if (typeof error === 'object') { - snackBar('warning', `${error.message}: Please Try again`) - dispatch({ type: END_LOADING }) - } else { - snackBar('error', error.response.data) - history('/') - } - dispatch({ type: CREATED_POST }) - } - callBack() -} - -export const updatePost = (id, post, snackBar,clear) => async (dispatch) => { - try { - await api.updatePost(id, post) - dispatch({ type: UPDATE, payload: post }) - if (snackBar) snackBar('info', 'Post updated successfully') - clear(); - } catch (error) { - snackBar('error', error) - } -} - -export const deletePost = (id, snackBar, callBack) => async (dispatch) => { - try { - dispatch({ type: DELETING_POST }) - await api.deletePost(id) - dispatch({ type: DELETE, payload: id }) - dispatch({ type: DELETED_POST }) - location.reload() - snackBar('info', 'Post deleted successfully') - } catch (error) { - snackBar('error', error) - } - callBack(false) -} diff --git a/client/src/actions/user.js b/client/src/actions/user.js deleted file mode 100644 index 9e6bddf8..00000000 --- a/client/src/actions/user.js +++ /dev/null @@ -1,14 +0,0 @@ -import { LOGOUT } from '../constants/actionTypes' -import * as api from '../api' - -export const updateUser = (formData, history, setUser, snackBar) => async (dispatch) => { - try { - await api.updateUser(formData) - dispatch({ type: LOGOUT }) - setUser(null) - snackBar('success', 'Update Successful ! Now Log in') - history('/') - } catch (error) { - snackBar('error', error.response.data.message) - } -} diff --git a/client/src/api/index.js b/client/src/api/index.js index 4ad9ee67..99e2f715 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -1,35 +1 @@ -import axios from 'axios' - -const apiURL = ['https://memories-pritam-server.vercel.app', 'http://localhost:5000'] -const API = axios.create({ baseURL: apiURL[0] }) - -API.interceptors.request.use((req) => { - const profile = localStorage.getItem('profile') - if (profile) { - req.headers.Authorization = `Bearer ${JSON.parse(profile).token}` - } - return req -}) - -export const fetchPosts = (page) => API.get(`/posts?page=${page}`) -export const fetchPost = (id) => API.get(`/posts/${id}`) -export const fetchPostsBySearch = (searchQuery) => API.get(`/posts/search?searchQuery=${searchQuery.search ?? 'none'}&tags=${searchQuery.tags ?? 'none'}`) -export const fetchComments = (postId) => API.get(`/comments/${postId}`) -export const fetchTags = () => API.get(`/posts/tags`) - -export const createPost = (newPost) => API.post('/posts', newPost) -export const createComment = (comment) => API.post('/comments/', comment) -export const updatePost = (id, updatedPost) => API.patch(`/posts/${id}`, updatedPost) -export const deletePost = (id) => API.delete(`/posts/${id}`) -export const deleteComment = (id) => API.delete(`/comments/${id}`) - -export const signIn = (formData) => API.post('/user/signin', formData) -export const googleSignIn = (formData) => API.post('/user/googleSignIn', formData) -export const signUp = (formData) => API.post('/user/signup', formData) -export const sendResetLink = (formData) => API.post('/user/forgotPassword', formData) -export const setNewPassword = (formData) => API.post('/user/resetPassword', formData) - -export const updateUser = (formData) => API.patch('/user/update', formData) -export const userDetails = (userId) => API.get(`/user/details/${userId}`) -export const fetchUserPostsByType = (userId, page, type) => API.get(`/user/posts/${userId}?page=${page}&type=${type}`) -export const fetchUserComments = (userId, page) => API.get(`/user/comments/${userId}?page=${page}`) +export * from './posts' diff --git a/client/src/api/posts.js b/client/src/api/posts.js new file mode 100644 index 00000000..902277f8 --- /dev/null +++ b/client/src/api/posts.js @@ -0,0 +1,106 @@ +import axios from 'axios' + +const baseURL = import.meta.env.VITE_API_URL +const api = axios.create({ baseURL }) + +api.interceptors.request.use(async (req) => { + try { + const session = await window.Clerk.session + if (!session) { + // not logged in + return req + } + const token = await session.getToken() + if (token) { + req.headers.Authorization = `Bearer ${token}` + } + } catch (error) { + console.error('Auth interceptor error:', error) + } + return req +}) + +const handleApiError = (error) => { + if (axios.isAxiosError(error)) { + const message = error.response?.data?.message || error.message + console.error('API Error:', { + status: error.response?.status, + message, + details: error.response?.data + }) + throw new Error(`API Error: ${message}`) + } + console.error('Unexpected error:', error) + throw error +} + +export const getPosts = async (cursor, limit) => { + try { + const { data } = await api.get('/posts', { params: { cursor, limit } }) + return data + } catch (error) { + throw handleApiError(error) + } +} + +export const searchPosts = async (query) => { + try { + const { data } = await api.get('/posts/search', { params: { query } }) + return data + } catch (error) { + throw handleApiError(error) + } +} + +export const getPost = async (id) => { + try { + const { data } = await api.get(`/posts/${id}`) + return data + } catch (error) { + throw handleApiError(error) + } +} + +export const createPost = async (post) => { + try { + const { data } = await api.post('/posts', post) + return data + } catch (error) { + throw handleApiError(error) + } +} + +export const updatePost = async (id, post) => { + try { + const { data } = await api.put(`/posts/${id}`, post) + return data + } catch (error) { + console.error(error) + throw handleApiError(error) + } +} + +export const deletePost = async (id) => { + try { + await api.delete(`/posts/${id}`) + } catch (error) { + throw handleApiError(error) + } +} + +export const reactPost = async (id, reaction) => { + try { + const { data } = await api.post(`/posts/${id}/react`, reaction) + return data + } catch (error) { + throw handleApiError(error) + } +} + +export const unreactPost = async (id) => { + try { + await api.delete(`/posts/${id}/react`) + } catch (error) { + throw handleApiError(error) + } +} diff --git a/client/src/images/background.jpg b/client/src/assets/bg.jpg similarity index 100% rename from client/src/images/background.jpg rename to client/src/assets/bg.jpg diff --git a/client/src/images/memories.png b/client/src/assets/brand.png similarity index 100% rename from client/src/images/memories.png rename to client/src/assets/brand.png diff --git a/client/src/assets/index.js b/client/src/assets/index.js new file mode 100644 index 00000000..ed262f39 --- /dev/null +++ b/client/src/assets/index.js @@ -0,0 +1,2 @@ +export { default as bg } from './bg.jpg' +export { default as brand } from './brand.png' diff --git a/client/src/components/AccountMenu.jsx b/client/src/components/AccountMenu.jsx new file mode 100644 index 00000000..e84d6cb9 --- /dev/null +++ b/client/src/components/AccountMenu.jsx @@ -0,0 +1,192 @@ +import { Box, Avatar, Menu, MenuItem, ListItemIcon, Divider, CircularProgress } from '@mui/material' +import { Settings, ChevronRight, Computer, DarkMode, Done, LightMode, Logout } from '@mui/icons-material' +import { useAuth, useUser } from '@clerk/clerk-react' +import { Link } from 'react-router-dom' +import { useTheme } from '@/hooks' +import { useState } from 'react' +import { UserAvatar } from '.' + +const AccountMenuItems = ({ handleClose, handleClick, open }) => { + const { user } = useUser() + const { signOut } = useAuth() + + return ( + <> + + {user.fullName} + + + + + + + + Settings + + + + + + + Theme + + + + + + + + + + Logout + + + ) +} + +const ThemeMenu = ({ handleClose, anchorEl, open }) => { + const { theme, setTheme } = useTheme() + const handleClick = (newTheme) => { + setTheme(newTheme) + handleClose() + } + + return ( + + handleClick('light')}> + + + + Light + {theme === 'light' && ( + + + + )} + + handleClick('dark')}> + + + + Dark + {theme === 'dark' && ( + + + + )} + + handleClick('system')}> + + + + System + {theme === 'system' && ( + + + + )} + + + ) +} + +const AccountMenu = () => { + const { isLoaded, isSignedIn } = useUser() + const [anchorEl2, setAnchorEl2] = useState(null) + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) + const handleClick = (event) => setAnchorEl(event.currentTarget) + const handleClose = () => setAnchorEl(null) + const handleClickTheme = (event) => setAnchorEl2(event.currentTarget) + const { user } = useUser() + + const handleCloseTheme = () => { + setAnchorEl2(null) + handleClose() + } + if (!isLoaded) { + return + } + if (!isSignedIn) { + return null + } + return ( + <> + + + + + + + ) +} +export default AccountMenu diff --git a/client/src/components/AppRouter.jsx b/client/src/components/AppRouter.jsx new file mode 100644 index 00000000..3e7a6f00 --- /dev/null +++ b/client/src/components/AppRouter.jsx @@ -0,0 +1,40 @@ +import { Routes, Route, useLocation, Navigate } from 'react-router-dom' +import { lazy, Suspense } from 'react' +import { AuthenticateWithRedirectCallback, useAuth } from '@clerk/clerk-react' +const { Posts, LogIn, NotFound, SignUp, VerifyEmail, Profile, Post } = { + Posts: lazy(() => import('@/pages/Posts')), + LogIn: lazy(() => import('@/pages/LogIn')), + NotFound: lazy(() => import('@/pages/NotFound')), + SignUp: lazy(() => import('@/pages/SignUp')), + VerifyEmail: lazy(() => import('@/pages/VerifyEmail')), + Profile: lazy(() => import('@/pages/Profile')), + Post: lazy(() => import('@/pages/Post')) +} +import { AuthRoute, PrivateRoute } from '@/routes' +import { SuspenseFallback } from '.' + +const AppRouter = () => { + const location = useLocation() + const { isLoaded } = useAuth() + if (!isLoaded) { + return + } + return ( + }> + + } /> + } /> + } /> + } />} /> + } /> + } />} /> + } />} /> + } />} /> + } />} /> + } /> + + + ) +} + +export default AppRouter diff --git a/client/src/components/Auth/Auth.jsx b/client/src/components/Auth/Auth.jsx deleted file mode 100644 index fda45a5c..00000000 --- a/client/src/components/Auth/Auth.jsx +++ /dev/null @@ -1,167 +0,0 @@ -import { jwtDecode } from 'jwt-decode' -import { useState, useEffect, useContext } from 'react' -import { useDispatch } from 'react-redux' -import { Avatar, Button, Paper, Grid, Typography, Container, Checkbox } from '@mui/material' -import { GoogleLogin as GoogleLogins } from '@react-oauth/google' -import { Link, useNavigate } from 'react-router-dom' -import { Root, classes } from './styles' -import { googleSignIn, signin, signup } from '../../actions/auth' -import LockOutlinedIcon from '@mui/icons-material/LockOutlined' -import Input from '../Input' -import UserIcon from '../UserIcon/UserIcon' -import { styled } from '@mui/material/styles' -import { SnackbarContext } from '../../contexts/SnackbarContext' -import { ModeContext } from '../../contexts/ModeContext' - -const initialState = { firstName: '', lastName: '', email: '', password: '', confirmPassword: '', remember: false } - -const Auth = () => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - - const [showPassword, setShowPassword] = useState(false) - const [isSignup, setIsSignUp] = useState(false) - const [formData, setFormData] = useState(initialState) - const [margin, setMargin] = useState('200px') - - const dispatch = useDispatch() - const history = useNavigate() - const handleShowPassword = () => setShowPassword((prevShowPassword) => !prevShowPassword) - const handleRememberMe = () => setFormData({ ...formData, remember: !formData.remember }) - const handleSubmit = (e) => { - e.preventDefault() - dispatch((isSignup ? signup : signin)(formData, history, snackBar)) - } - const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) - const switchMode = () => { - setIsSignUp((prevIsSignUp) => !prevIsSignUp) - setShowPassword(false) - } - const googleSuccess = ({ credential: token }) => { - try { - const { email, family_name: familyName, given_name: givenName, sub, picture: image, name } = jwtDecode(token) - const googleId = sub.padStart(24, '0') - const result = { email, familyName, givenName, googleId, image, name } - dispatch(googleSignIn({ result, token }, history, snackBar)) - } catch (error) { - snackBar('error', error) - } - } - const googleFailure = ({ error }) => { - console.log(error) - if (error === 'popup_closed_by_user') { - return snackBar('warning', 'PopUp Closed By User') - } - snackBar('error', 'Google Sign In was unsuccessful. Try Again Later') - } - - useEffect(() => setMargin(isSignup ? '50px' : '200px'), [isSignup]) - - const { mode } = useContext(ModeContext); - - return ( - - - - {isSignup ? ( - - ) : ( - - - - )} - - {isSignup ? 'Sign Up' : 'Sign In'} - -
- - {isSignup && ( - <> - - - - )} - - - {isSignup && } - - - -
- -
- - - - - - - - - -
-
-
-
- ) -} - -const BpIcon = styled('span')(({ theme }) => ({ - borderRadius: 3, - width: 16, - height: 16, - boxShadow: theme.palette.mode === 'dark' ? '0 0 0 1px rgb(16 22 26 / 40%)' : 'inset 0 0 0 1px rgba(16,22,26,.2), inset 0 -1px 0 rgba(16,22,26,.1)', - backgroundColor: theme.palette.mode === 'dark' ? '#394b59' : '#f5f8fa', - backgroundImage: theme.palette.mode === 'dark' ? 'linear-gradient(180deg,hsla(0,0%,100%,.05),hsla(0,0%,100%,0))' : 'linear-gradient(180deg,hsla(0,0%,100%,.8),hsla(0,0%,100%,0))', - '.Mui-focusVisible &': { - outline: '2px auto rgba(19,124,189,.6)', - outlineOffset: 2, - }, - 'input:hover ~ &': { - backgroundColor: theme.palette.mode === 'dark' ? '#30404d' : '#ebf1f5', - }, - 'input:disabled ~ &': { - boxShadow: 'none', - background: theme.palette.mode === 'dark' ? 'rgba(57,75,89,.5)' : 'rgba(206,217,224,.5)', - }, -})) - -const BpCheckedIcon = styled(BpIcon)({ - backgroundColor: '#137cbd', - backgroundImage: 'linear-gradient(180deg,hsla(0,0%,100%,.1),hsla(0,0%,100%,0))', - '&:before': { - display: 'block', - width: 16, - height: 16, - backgroundImage: "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath" + " fill-rule='evenodd' clip-rule='evenodd' d='M12 5c-.28 0-.53.11-.71.29L7 9.59l-2.29-2.3a1.003 " + "1.003 0 00-1.42 1.42l3 3c.18.18.43.29.71.29s.53-.11.71-.29l5-5A1.003 1.003 0 0012 5z' fill='%23fff'/%3E%3C/svg%3E\")", - content: '""', - }, - 'input:hover ~ &': { - backgroundColor: '#106ba3', - }, -}) - -const BpCheckbox = (props) => { - return ( - } - icon={} - inputProps={{ 'aria-label': 'Checkbox demo' }} - {...props} - /> - ) -} - -export default Auth diff --git a/client/src/components/Auth/ForgotPassword/index.jsx b/client/src/components/Auth/ForgotPassword/index.jsx deleted file mode 100644 index 402509bf..00000000 --- a/client/src/components/Auth/ForgotPassword/index.jsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState, useEffect, useContext } from 'react' -import { useDispatch } from 'react-redux' -import { Avatar, Button, Paper, Grid, Typography, Container, CircularProgress } from '@mui/material' -import { LockReset, Pattern } from '@mui/icons-material' -import { useNavigate, useParams } from 'react-router-dom' -import { Root, classes } from './styles' -import { forgotPassword, setNewPassword } from '../../../actions/auth' -import Input from '../../Input' -import { SnackbarContext } from '../../../contexts/SnackbarContext' -import { ModeContext } from '../../../contexts/ModeContext' - -const initialState = { email: '' } - -const ForgotPassword = () => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const [showPassword, setShowPassword] = useState(false) - const handleShowPassword = () => setShowPassword((prevShowPassword) => !prevShowPassword) - const [loading, setLoading] = useState(false) - const [formData, setFormData] = useState(initialState) - const params = useParams() - const [resetPassword, setResetPassword] = useState(params.id && params.token) - const dispatch = useDispatch() - const history = useNavigate() - const handleSubmit = (e) => { - e.preventDefault() - setLoading(true) - const _function = resetPassword ? setNewPassword : forgotPassword - dispatch(_function(formData, history, snackBar, setLoading)) - } - const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) - useEffect(() => { - if (params.id && params.token) { - formData.id = params.id - formData.token = params.token - setResetPassword(true) - } - }, [params]) - - const { mode } = useContext(ModeContext); - - return ( - - - - {resetPassword ? : } - {resetPassword ? 'Reset Password' : 'Account Recovery'} -
- - {resetPassword ? ( // - - ) : ( - - )} - - -
-
-
-
- ) -} - -export default ForgotPassword diff --git a/client/src/components/Auth/ForgotPassword/styles.js b/client/src/components/Auth/ForgotPassword/styles.js deleted file mode 100644 index 4d7d541c..00000000 --- a/client/src/components/Auth/ForgotPassword/styles.js +++ /dev/null @@ -1,62 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'ForgotPassword' -export const classes = { - root: `${PREFIX}-root`, - paperLight: `${PREFIX}-paperLight`, - paperDark: `${PREFIX}-paperDark`, - avatar: `${PREFIX}-avtar`, - form: `${PREFIX}-form`, - submit: `${PREFIX}-submit`, -} - -export const Root = styled('div')(({ theme, reset }) => ({ - [`&.${classes.root}`]: { - '& .MuiFormLabel-root': { - color: 'white', - }, - }, - - [`& .${classes.paperLight}`]: { - marginBottom: theme.spacing(3), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(2), - transition: '0.2s', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - [`& .${classes.paperDark}`]: { - marginBottom: theme.spacing(3), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(2), - transition: '0.2s', - backgroundColor: 'rgba(5, 5, 5, .9)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - backgroundColor: reset ? '#42a5f5' : '#4caf50', - }, - [`& .${classes.form}`]: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(3), - }, - [`& .${classes.submit}`]: { - margin: theme.spacing(1, 0, 1, 0), - backgroundColor: reset ? '#42a5f5' : '#4caf50', - display: 'flex', - gap: 10, - '&:hover': { - backgroundColor: reset ? '#256597' : '#2d6d30', - }, - }, -})) - -export default Root diff --git a/client/src/components/Auth/styles.js b/client/src/components/Auth/styles.js deleted file mode 100644 index 625610f8..00000000 --- a/client/src/components/Auth/styles.js +++ /dev/null @@ -1,58 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Auth' -export const classes = { - root: `${PREFIX}-root`, - paperLight: `${PREFIX}-paperLight`, - paperDark: `${PREFIX}-paperDark`, - avatar: `${PREFIX}-avtar`, - form: `${PREFIX}-form`, - submit: `${PREFIX}-submit`, - googleButton: `${PREFIX}-googleButton`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - '& .MuiFormLabel-root': { - color: 'white', - }, - }, - [`& .${classes.paperLight}`]: { - marginBottom: theme.spacing(3), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(2), - transition: '0.2s', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - [`& .${classes.paperDark}`]: { - marginBottom: theme.spacing(3), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(2), - transition: '0.2s', - backgroundColor: 'rgba(5, 5, 5, .9)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - backgroundColor: theme.palette.secondary.main, - }, - [`& .${classes.form}`]: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(3), - }, - [`& .${classes.submit}`]: { - margin: theme.spacing(1, 0, 1, 0), - }, - [`& .${classes.googleButton}`]: { - marginBottom: theme.spacing(2), - }, -})) - -export default Root diff --git a/client/src/components/Bottombar.jsx b/client/src/components/Bottombar.jsx new file mode 100644 index 00000000..bfd61ff9 --- /dev/null +++ b/client/src/components/Bottombar.jsx @@ -0,0 +1,52 @@ +import { Add, Favorite, MessageSharp } from '@mui/icons-material' +import { Avatar, BottomNavigation, BottomNavigationAction } from '@mui/material' +import { useState } from 'react' +import { CreatePostDialog } from '@/components' + +const Bottombar = () => { + const [open, setOpen] = useState(false) + const handleClickOpen = () => setOpen(true) + + return ( + + } /> + + + + } + /> + } /> + + + ) +} + +export default Bottombar diff --git a/client/src/components/ChipInput/ChipInput.js b/client/src/components/ChipInput/ChipInput.js deleted file mode 100644 index 31080a51..00000000 --- a/client/src/components/ChipInput/ChipInput.js +++ /dev/null @@ -1,1137 +0,0 @@ -/* eslint-disable */ -// Object.defineProperty(exports, '__esModule', { -// value: true, -// }) -// const _defaultChipRenderer = exports.default = void 0 -// export { _defaultChipRenderer as defaultChipRenderer } - -import _react from 'react' -import _reactDom from "react-dom" -import _propTypes from 'prop-types' -// var _reactDom = _interopRequireDefault(require('react-dom')) - -// var _propTypes = _interopRequireDefault(require('prop-types')) -var _Input = _interopRequireDefault(require('@mui/material/Input')) -var _FilledInput = _interopRequireDefault(require('@mui/material/FilledInput')) -var _OutlinedInput = _interopRequireDefault(require('@mui/material/OutlinedInput')) -var _InputLabel = _interopRequireDefault(require('@mui/material/InputLabel')) -var _Chip = _interopRequireDefault(require('@mui/material/Chip')) -var _FormControl = _interopRequireDefault(require('@mui/material/FormControl')) -var _FormHelperText = _interopRequireDefault(require('@mui/material/FormHelperText')) -var _blue = _interopRequireDefault(require('@mui/material/colors/blue')) - -var _withStyles = _interopRequireDefault(require('@material-ui/core/styles/withStyles')) -// var _withStyles = _interopRequireDefault(require("@mui/styles/withStyles/withStyles")); - -var _classnames = _interopRequireDefault(require('classnames')) - -function _interopRequireDefault(obj) { - return obj && obj.__esModule ? obj : { default: obj } -} - -function _typeof(obj) { - if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') { - _typeof = function _typeof(obj) { - return typeof obj - } - } else { - _typeof = function _typeof(obj) { - return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj - } - } - return _typeof(obj) -} - -function _extends() { - _extends = - Object.assign || - function (target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i] - for (var key in source) { - if (Object.prototype.hasOwnProperty.call(source, key)) { - target[key] = source[key] - } - } - } - return target - } - return _extends.apply(this, arguments) -} - -function _objectWithoutProperties(source, excluded) { - if (source === null) return {} - var target = _objectWithoutPropertiesLoose(source, excluded) - var key, i - if (Object.getOwnPropertySymbols) { - var sourceSymbolKeys = Object.getOwnPropertySymbols(source) - for (i = 0; i < sourceSymbolKeys.length; i++) { - key = sourceSymbolKeys[i] - if (excluded.indexOf(key) >= 0) continue - if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue - target[key] = source[key] - } - } - return target -} - -function _objectWithoutPropertiesLoose(source, excluded) { - if (source === null) return {} - var target = {} - var sourceKeys = Object.keys(source) - var key, i - for (i = 0; i < sourceKeys.length; i++) { - key = sourceKeys[i] - if (excluded.indexOf(key) >= 0) continue - target[key] = source[key] - } - return target -} - -function _objectSpread(target) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i] !== null ? arguments[i] : {} - var ownKeys = Object.keys(source) - if (typeof Object.getOwnPropertySymbols === 'function') { - ownKeys = ownKeys.concat( - Object.getOwnPropertySymbols(source).filter(function (sym) { - return Object.getOwnPropertyDescriptor(source, sym).enumerable - }) - ) - } - ownKeys.forEach(function (key) { - _defineProperty(target, key, source[key]) - }) - } - return target -} - -function _toConsumableArray(arr) { - return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread() -} - -function _nonIterableSpread() { - throw new TypeError('Invalid attempt to spread non-iterable instance') -} - -function _iterableToArray(iter) { - if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === '[object Arguments]') return Array.from(iter) -} - -function _arrayWithoutHoles(arr) { - if (Array.isArray(arr)) { - for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { - arr2[i] = arr[i] - } - return arr2 - } -} - -function _classCallCheck(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError('Cannot call a class as a function') - } -} - -function _defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i] - descriptor.enumerable = descriptor.enumerable || false - descriptor.configurable = true - if ('value' in descriptor) descriptor.writable = true - Object.defineProperty(target, descriptor.key, descriptor) - } -} - -function _createClass(Constructor, protoProps, staticProps) { - if (protoProps) _defineProperties(Constructor.prototype, protoProps) - if (staticProps) _defineProperties(Constructor, staticProps) - return Constructor -} - -function _possibleConstructorReturn(self, call) { - if (call && (_typeof(call) === 'object' || typeof call === 'function')) { - return call - } - return _assertThisInitialized(self) -} - -function _getPrototypeOf(o) { - _getPrototypeOf = Object.setPrototypeOf - ? Object.getPrototypeOf - : function _getPrototypeOf(o) { - return o.__proto__ || Object.getPrototypeOf(o) - } - return _getPrototypeOf(o) -} - -function _inherits(subClass, superClass) { - if (typeof superClass !== 'function' && superClass !== null) { - throw new TypeError('Super expression must either be null or a function') - } - subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }) - if (superClass) _setPrototypeOf(subClass, superClass) -} - -function _setPrototypeOf(o, p) { - _setPrototypeOf = - Object.setPrototypeOf || - function _setPrototypeOf(o, p) { - o.__proto__ = p - return o - } - return _setPrototypeOf(o, p) -} - -function _assertThisInitialized(self) { - if (self === void 0) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called") - } - return self -} - -function _defineProperty(obj, key, value) { - if (key in obj) { - Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }) - } else { - obj[key] = value - } - return obj -} - -var variantComponent = { - standard: _Input.default, - filled: _FilledInput.default, - outlined: _OutlinedInput.default, -} - -var styles = function styles(theme) { - var light = theme.palette.type === 'light' - var bottomLineColor = light ? 'rgba(0, 0, 0, 0.42)' : 'rgba(255, 255, 255, 0.7)' - return { - root: {}, - inputRoot: { - display: 'inline-flex', - flexWrap: 'wrap', - flex: 1, - marginTop: 0, - minWidth: 70, - '&$outlined,&$filled': { - boxSizing: 'border-box', - }, - '&$outlined': { - paddingTop: 14, - }, - '&$filled': { - paddingTop: 28, - }, - }, - input: { - display: 'inline-block', - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap', - appearance: 'none', - // Remove border in Safari, doesn't seem to break anything in other browsers - WebkitTapHighlightColor: 'rgba(0,0,0,0)', - // Remove mobile color flashing (deprecated style). - float: 'left', - flex: 1, - }, - chipContainer: { - display: 'flex', - flexFlow: 'row wrap', - cursor: 'text', - marginBottom: -2, - minHeight: 40, - '&$labeled&$standard': { - marginTop: 18, - }, - }, - outlined: { - '& input': { - height: 16, - paddingTop: 4, - paddingBottom: 12, - marginTop: 4, - marginBottom: 4, - }, - }, - standard: {}, - filled: { - '& input': { - height: 22, - marginBottom: 4, - marginTop: 4, - paddingTop: 0, - }, - '$marginDense & input': { - height: 26, - }, - }, - labeled: {}, - label: { - top: 4, - '&$outlined&:not($labelShrink)': { - top: 2, - '$marginDense &': { - top: 5, - }, - }, - '&$filled&:not($labelShrink)': { - top: 15, - '$marginDense &': { - top: 20, - }, - }, - }, - labelShrink: { - top: 0, - }, - helperText: { - marginBottom: -20, - }, - focused: {}, - disabled: {}, - underline: { - '&:after': { - borderBottom: '2px solid '.concat(theme.palette.primary[light ? 'dark' : 'light']), - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '""', - position: 'absolute', - right: 0, - transform: 'scaleX(0)', - transition: theme.transitions.create('transform', { - duration: theme.transitions.duration.shorter, - easing: theme.transitions.easing.easeOut, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&$focused:after': { - transform: 'scaleX(1)', - }, - '&$error:after': { - borderBottomColor: theme.palette.error.main, - transform: 'scaleX(1)', // error is always underlined in red - }, - '&:before': { - borderBottom: '1px solid '.concat(bottomLineColor), - left: 0, - bottom: 0, - // Doing the other way around crash on IE 11 "''" https://github.com/cssinjs/jss/issues/242 - content: '"\\00a0"', - position: 'absolute', - right: 0, - transition: theme.transitions.create('border-bottom-color', { - duration: theme.transitions.duration.shorter, - }), - pointerEvents: 'none', // Transparent to the hover style. - }, - '&:hover:not($disabled):not($focused):not($error):before': { - borderBottom: '2px solid '.concat(theme.palette.text.primary), - // Reset on touch devices, it doesn't add specificity - '@media (hover: none)': { - borderBottom: '1px solid '.concat(bottomLineColor), - }, - }, - '&$disabled:before': { - borderBottomStyle: 'dotted', - }, - }, - error: { - '&:after': { - backgroundColor: theme.palette.error.main, - transform: 'scaleX(1)', // error is always underlined in red - }, - }, - chip: { - margin: '0 8px 8px 0', - float: 'left', - }, - marginDense: {}, - } -} - -var keyCodes = { - BACKSPACE: 8, - DELETE: 46, - LEFT_ARROW: 37, - RIGHT_ARROW: 39, -} - -const ChipInput = - /*#__PURE__*/ - (function (_React$Component) { - _inherits(ChipInput, _React$Component) - - function ChipInput(props) { - // skipcq: JS-0123 - var _this - - _classCallCheck(this, ChipInput) - - _this = _possibleConstructorReturn(this, _getPrototypeOf(ChipInput).call(this, props)) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'state', { - chips: [], - errorText: undefined, - focusedChip: null, - inputValue: '', - isClean: true, - isFocused: false, - chipsUpdated: false, - prevPropsValue: [], - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'focus', function () { - _this.actualInput.focus() - - if (_this.state.focusedChip !== null) { - _this.setState({ - focusedChip: null, - }) - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleInputBlur', function (event) { - if (_this.props.onBlur) { - _this.props.onBlur(event) - } - - _this.setState({ - isFocused: false, - }) - - if (_this.state.focusedChip !== null) { - _this.setState({ - focusedChip: null, - }) - } - - var value = event.target.value - - switch (_this.props.blurBehavior) { - case 'add': - if (_this.props.delayBeforeAdd) { - // Lets assume that we only want to add the existing content as chip, when - // another event has not added a chip within 200ms . - // e.g. onSelection Callback in Autocomplete case - var numChipsBefore = (_this.props.value || _this.state.chips).length - _this.inputBlurTimeout = setTimeout(function () { - var numChipsAfter = (_this.props.value || _this.state.chips).length - - if (numChipsBefore === numChipsAfter) { - _this.handleAddChip(value) - } else { - _this.clearInput() - } - }, 150) - } else { - _this.handleAddChip(value) - } - - break - - case 'clear': - _this.clearInput() - - break - default: - break - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleInputFocus', function (event) { - _this.setState({ - isFocused: true, - }) - - if (_this.props.onFocus) { - _this.props.onFocus(event) - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleKeyDown', function (event) { - var focusedChip = _this.state.focusedChip - _this._keyPressed = false - _this._preventChipCreation = false - - if (_this.props.onKeyDown) { - // Needed for arrow controls on menu in autocomplete scenario - _this.props.onKeyDown(event) // Check if the callback marked the event as isDefaultPrevented() and skip further actions - // enter key for example should not always add the current value of the inputField - - if (event.isDefaultPrevented()) { - return - } - } - - var chips = _this.props.value || _this.state.chips - - if (_this.props.newChipKeyCodes.indexOf(event.keyCode) >= 0 || _this.props.newChipKeys.indexOf(event.key) >= 0) { - var result = _this.handleAddChip(event.target.value) - - if (result !== false) { - event.preventDefault() - } - - return - } - - switch (event.keyCode) { - case keyCodes.BACKSPACE: - if (event.target.value === '') { - if (focusedChip !== null) { - _this.handleDeleteChip(chips[focusedChip], focusedChip) - - if (focusedChip > 0) { - _this.setState({ - focusedChip: focusedChip - 1, - }) - } - } else { - _this.setState({ - focusedChip: chips.length - 1, - }) - } - } - - break - - case keyCodes.DELETE: - if (event.target.value === '' && focusedChip !== null) { - _this.handleDeleteChip(chips[focusedChip], focusedChip) - - if (focusedChip <= chips.length - 1) { - _this.setState({ - focusedChip: focusedChip, - }) - } - } - - break - - case keyCodes.LEFT_ARROW: - if (focusedChip === null && event.target.value === '' && chips.length) { - _this.setState({ - focusedChip: chips.length - 1, - }) - } else if (focusedChip !== null && focusedChip > 0) { - _this.setState({ - focusedChip: focusedChip - 1, - }) - } - - break - - case keyCodes.RIGHT_ARROW: - if (focusedChip !== null && focusedChip < chips.length - 1) { - _this.setState({ - focusedChip: focusedChip + 1, - }) - } else { - _this.setState({ - focusedChip: null, - }) - } - - break - - default: - _this.setState({ - focusedChip: null, - }) - - break - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleKeyUp', function (event) { - if (!_this._preventChipCreation && (_this.props.newChipKeyCodes.indexOf(event.keyCode) >= 0 || _this.props.newChipKeys.indexOf(event.key) >= 0) && _this._keyPressed) { - _this.clearInput() - } else { - _this.updateInput(event.target.value) - } - - if (_this.props.onKeyUp) { - _this.props.onKeyUp(event) - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleKeyPress', function (event) { - _this._keyPressed = true - - if (_this.props.onKeyPress) { - _this.props.onKeyPress(event) - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'handleUpdateInput', function (e) { - if (_this.props.inputValue == null) { - //Dont change '==' to '===' - _this.updateInput(e.target.value) - } - - if (_this.props.onUpdateInput) { - _this.props.onUpdateInput(e) - } - }) - - _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), 'setActualInputRef', function (ref) { - _this.actualInput = ref - - if (_this.props.inputRef) { - _this.props.inputRef(ref) - } - }) - - if (props.defaultValue) { - _this.state.chips = props.defaultValue - } - - _this.labelRef = _react.default.createRef() - _this.input = _react.default.createRef() - return _this - } - - _createClass( - ChipInput, - [ - { - key: 'componentDidMount', - value: function componentDidMount() { - if (this.props.variant === 'outlined') { - this.labelNode = _reactDom.default.findDOMNode(this.labelRef.current) - this.forceUpdate() - } - }, - }, - { - key: 'componentWillUnmount', - value: function componentWillUnmount() { - clearTimeout(this.inputBlurTimeout) - }, - }, - { - key: 'blur', - - /** - * Blurs this component. - * @public - */ - value: function blur() { - if (this.input) this.actualInput.blur() - }, - /** - * Focuses this component. - * @public - */ - }, - { - key: 'handleAddChip', - - /** - * Handles adding a chip. - * @param {string|object} chip Value of the chip, either a string or an object (if dataSourceConfig is set) - * @returns True if the chip was added (or at least `onAdd` was called), false if adding the chip was prevented - */ - value: function handleAddChip(chip) { - var _this2 = this - - if (this.props.onBeforeAdd && !this.props.onBeforeAdd(chip)) { - this._preventChipCreation = true - return false - } - - this.clearInput() - var chips = this.props.value || this.state.chips - - if (this.props.dataSourceConfig) { - if (typeof chip === 'string') { - var _chip - - chip = ((_chip = {}), _defineProperty(_chip, this.props.dataSourceConfig.text, chip), _defineProperty(_chip, this.props.dataSourceConfig.value, chip), _chip) - } - - if ( - this.props.allowDuplicates || - !chips.some(function (c) { - return c[_this2.props.dataSourceConfig.value] === chip[_this2.props.dataSourceConfig.value] - }) - ) { - if (this.props.value && this.props.onAdd) { - this.props.onAdd(chip) - } else { - this.updateChips([].concat(_toConsumableArray(this.state.chips), [chip])) - } - } - - return true - } - - if (chip.trim().length > 0) { - if (this.props.allowDuplicates || chips.indexOf(chip) === -1) { - if (this.props.value && this.props.onAdd) { - this.props.onAdd(chip) - } else { - this.updateChips([].concat(_toConsumableArray(this.state.chips), [chip])) - } - } - - return true - } - - return false - }, - }, - { - key: 'handleDeleteChip', - value: function handleDeleteChip(chip, i) { - if (!this.props.value) { - var chips = this.state.chips.slice() - var changed = chips.splice(i, 1) // remove the chip at index i - - if (changed) { - var focusedChip = this.state.focusedChip - - if (this.state.focusedChip === i) { - focusedChip = null - } else if (this.state.focusedChip > i) { - focusedChip = this.state.focusedChip - 1 - } - - this.updateChips(chips, { - focusedChip: focusedChip, - }) - } - } else if (this.props.onDelete) { - this.props.onDelete(chip, i) - } - }, - }, - { - key: 'updateChips', - value: function updateChips(chips) { - var additionalUpdates = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {} - this.setState( - _objectSpread( - { - chips: chips, - chipsUpdated: true, - }, - additionalUpdates - ) - ) - - if (this.props.onChange) { - this.props.onChange(chips) - } - }, - /** - * Clears the text field for adding new chips. - * This only works in uncontrolled input mode, i.e. if the inputValue prop is not used. - * @public - */ - }, - { - key: 'clearInput', - value: function clearInput() { - this.updateInput('') - }, - }, - { - key: 'updateInput', - value: function updateInput(value) { - this.setState({ - inputValue: value, - }) - }, - /** - * Set the reference to the actual input, that is the input of the Input. - * @param {object} ref - The reference - */ - }, - { - key: 'render', - value: function render() { - var _this3 = this, - _cx2 - - var _this$props = this.props, - alwaysShowPlaceholder = _this$props.alwaysShowPlaceholder, - _this$props$chipRende = _this$props.chipRenderer, - chipRenderer = _this$props$chipRende === void 0 ? defaultChipRenderer : _this$props$chipRende, - classes = _this$props.classes, - className = _this$props.className, - dataSourceConfig = _this$props.dataSourceConfig, - disabled = _this$props.disabled, - disableUnderline = _this$props.disableUnderline, - error = _this$props.error, - FormHelperTextProps = _this$props.FormHelperTextProps, - fullWidth = _this$props.fullWidth, - fullWidthInput = _this$props.fullWidthInput, - helperText = _this$props.helperText, - id = _this$props.id, - _this$props$InputProp = _this$props.InputProps, - InputProps = _this$props$InputProp === void 0 ? {} : _this$props$InputProp, - _this$props$InputLabe = _this$props.InputLabelProps, - InputLabelProps = _this$props$InputLabe === void 0 ? {} : _this$props$InputLabe, - inputValue = _this$props.inputValue, - label = _this$props.label, - placeholder = _this$props.placeholder, - readOnly = _this$props.readOnly, - required = _this$props.required, - rootRef = _this$props.rootRef, - value = _this$props.value, - variant = _this$props.variant, - other = _objectWithoutProperties(_this$props, [ - 'allowDuplicates', - 'alwaysShowPlaceholder', - 'blurBehavior', - 'children', - 'chipRenderer', - 'classes', - 'className', - 'clearInputValueOnChange', - 'dataSource', - 'dataSourceConfig', - 'defaultValue', - 'delayBeforeAdd', - 'disabled', - 'disableUnderline', - 'error', - 'filter', - 'FormHelperTextProps', - 'fullWidth', - 'fullWidthInput', - 'helperText', - 'id', - 'InputProps', - 'inputRef', - 'InputLabelProps', - 'inputValue', - 'label', - 'newChipKeyCodes', - 'newChipKeys', - 'onBeforeAdd', - 'onAdd', - 'onBlur', - 'onDelete', - 'onChange', - 'onFocus', - 'onKeyDown', - 'onKeyPress', - 'onKeyUp', - 'onUpdateInput', - 'placeholder', - 'readOnly', - 'required', - 'rootRef', - 'value', - 'variant', - ]) - - var chips = value || this.state.chips - // ignore: '!=' to '!==' - var actualInputValue = inputValue != null ? inputValue : this.state.inputValue // skipcq: JS-0059, JS-0050 - var hasInput = (this.props.value || actualInputValue).length > 0 || actualInputValue.length > 0 - const shrinkFloatingLabel = InputLabelProps.shrink != null ? InputLabelProps.shrink : label !== null && (hasInput || this.state.isFocused || chips.length > 0) // skipcq: JS-0059, JS-0050 - var chipComponents = chips.map(function (chip, i) { - var value = dataSourceConfig ? chip[dataSourceConfig.value] : chip - return chipRenderer( - { - value: value, - text: dataSourceConfig ? chip[dataSourceConfig.text] : chip, - chip: chip, - isDisabled: Boolean(disabled), - isReadOnly: readOnly, - isFocused: _this3.state.focusedChip === i, - handleClick: function handleClick() { - return _this3.setState({ - focusedChip: i, - }) - }, - handleDelete: function handleDelete() { - return _this3.handleDeleteChip(chip, i) - }, - className: classes.chip, - }, - i - ) - }) - var InputMore = {} - - if (variant === 'outlined') { - InputMore.notched = shrinkFloatingLabel - InputMore.labelwidth = (shrinkFloatingLabel && this.labelNode && this.labelNode.offsetWidth) || 0 - } - - if (variant !== 'standard') { - InputMore.startAdornment = _react.default.createElement(_react.default.Fragment, null, chipComponents) - } else { - InputProps.disableUnderline = true - } - - var InputComponent = variantComponent[variant] - return _react.default.createElement( - _FormControl.default, - _extends( - { - ref: rootRef, - fullWidth: fullWidth, - className: (0, _classnames.default)(className, classes.root, _defineProperty({}, classes.marginDense, other.margin === 'dense')), - error: error, - required: required, - onClick: this.focus, - disabled: disabled, - variant: variant, - }, - other - ), - label && - _react.default.createElement( - _InputLabel.default, - _extends( - { - htmlFor: id, - classes: { - root: (0, _classnames.default)(classes[variant], classes.label), - shrink: classes.labelShrink, - }, - shrink: shrinkFloatingLabel, - focused: this.state.isFocused, - variant: variant, - ref: this.labelRef, - }, - InputLabelProps - ), - label - ), - _react.default.createElement( - 'div', - { - className: (0, _classnames.default)(classes[variant], classes.chipContainer, ((_cx2 = {}), _defineProperty(_cx2, classes.focused, this.state.isFocused), _defineProperty(_cx2, classes.underline, !disableUnderline && variant === 'standard'), _defineProperty(_cx2, classes.disabled, disabled), _defineProperty(_cx2, classes.labeled, label !== null), _defineProperty(_cx2, classes.error, error), _cx2)), - }, - variant === 'standard' && chipComponents, - _react.default.createElement( - InputComponent, - _extends( - { - ref: this.input, - classes: { - input: (0, _classnames.default)(classes.input, classes[variant]), - root: (0, _classnames.default)(classes.inputRoot, classes[variant]), - }, - id: id, - value: actualInputValue, - onChange: this.handleUpdateInput, - onKeyDown: this.handleKeyDown, - onKeyPress: this.handleKeyPress, - onKeyUp: this.handleKeyUp, - onFocus: this.handleInputFocus, - onBlur: this.handleInputBlur, - inputRef: this.setActualInputRef, - disabled: disabled, - fullWidth: fullWidthInput, - placeholder: (!hasInput && (shrinkFloatingLabel || label === null)) || alwaysShowPlaceholder ? placeholder : null, - readOnly: readOnly, - }, - InputProps, - InputMore - ) - ) - ), - helperText && - _react.default.createElement( - _FormHelperText.default, - _extends({}, FormHelperTextProps, { - className: FormHelperTextProps ? (0, _classnames.default)(FormHelperTextProps.className, classes.helperText) : classes.helperText, - }), - helperText - ) - ) - }, - }, - ], - [ - { - key: 'getDerivedStateFromProps', - value: function getDerivedStateFromProps(props, state) { - var newState = null - - if (props.value && props.value.length !== state.prevPropsValue.length) { - newState = { - prevPropsValue: props.value, - } - - if (props.clearInputValueOnChange) { - newState.inputValue = '' - } - } // if change detection is only needed for clearInputValueOnChange - - if (props.clearInputValueOnChange && props.value && props.value.length !== state.prevPropsValue.length) { - newState = { - prevPropsValue: props.value, - inputValue: '', - } - } - - if (props.disabled) { - newState = _objectSpread({}, newState, { - focusedChip: null, - }) - } - - if (!state.chipsUpdated && props.defaultValue) { - newState = _objectSpread({}, newState, { - chips: props.defaultValue, - }) - } - - return newState - }, - }, - ] - ) - - return ChipInput - })(_react.default.Component) - -ChipInput.propTypes = { - /** Allows duplicate chips if set to true. */ - allowDuplicates: _propTypes.default.bool, - - /** If true, the placeholder will always be visible. */ - alwaysShowPlaceholder: _propTypes.default.bool, - - /** Behavior when the chip input is blurred: `'clear'` clears the input, `'add'` creates a chip and `'ignore'` keeps the input. */ - blurBehavior: _propTypes.default.oneOf(['clear', 'add', 'ignore']), - - /** A function of the type `({ value, text, chip, isFocused, isDisabled, isReadOnly, handleClick, handleDelete, className }, key) => node` that returns a chip based on the given properties. This can be used to customize chip. Each item in the `dataSource` array will be passed to `chipRenderer` as arguments `chip`, `value` and `text`. If `dataSource` is an array of objects and `dataSourceConfig` is present, then `value` and `text` will instead correspond to the object values defined in `dataSourceConfig`. If `dataSourceConfig` is not set and `dataSource` is an array of objects, then a custom `chipRenderer` must be set. `chip` is always the raw value from `dataSource`, either an object or a string. */ - chipRenderer: _propTypes.default.func, - - /** Whether the input value should be cleared if the `value` prop is changed. */ - clearInputValueOnChange: _propTypes.default.bool, - - /** Data source for auto complete. This should be an array of strings or objects. */ - dataSource: _propTypes.default.array, - - /** Config for objects list dataSource, e.g. `{ text: 'text', value: 'value' }`. If not specified, the `dataSource` must be a flat array of strings or a custom `chipRenderer` must be set to handle the objects. */ - dataSourceConfig: _propTypes.default.shape({ - text: _propTypes.default.string.isRequired, - value: _propTypes.default.string.isRequired, - }), - - /** The chips to display by default (for uncontrolled mode). */ - defaultValue: _propTypes.default.array, - - /** Whether to use `setTimeout` to delay adding chips in case other input events like `onSelection` need to fire first */ - delayBeforeAdd: _propTypes.default.bool, - - /** Disables the chip input if set to true. */ - disabled: _propTypes.default.bool, - - /** Disable the input underline. Only valid for 'standard' variant */ - disableUnderline: _propTypes.default.bool, - - /** Props to pass through to the `FormHelperText` component. */ - FormHelperTextProps: _propTypes.default.object, - - /** If true, the chip input will fill the available width. */ - fullWidth: _propTypes.default.bool, - - /** If true, the input field will always be below the chips and fill the available space. By default, it will try to be beside the chips. */ - fullWidthInput: _propTypes.default.bool, - - /** Helper text that is displayed below the input. */ - helperText: _propTypes.default.node, - - /** Props to pass through to the `InputLabel`. */ - InputLabelProps: _propTypes.default.object, - - /** Props to pass through to the `Input`. */ - InputProps: _propTypes.default.object, - - /** Use this property to pass a ref callback to the native input component. */ - inputRef: _propTypes.default.func, - - /** The input value (enables controlled mode for the text input if set). */ - inputValue: _propTypes.default.string, - - /* The content of the floating label. */ - label: _propTypes.default.node, - - /** The key codes (`KeyboardEvent.keyCode`) used to determine when to create a new chip. */ - newChipKeyCodes: _propTypes.default.arrayOf(_propTypes.default.number), - - /** The keys (`KeyboardEvent.key`) used to determine when to create a new chip. */ - newChipKeys: _propTypes.default.arrayOf(_propTypes.default.string), - - /** Callback function that is called when a new chip was added (in controlled mode). */ - onAdd: _propTypes.default.func, - - /** Callback function that is called with the chip to be added and should return true to add the chip or false to prevent the chip from being added without clearing the text input. */ - onBeforeAdd: _propTypes.default.func, - - /** Callback function that is called when the chips change (in uncontrolled mode). */ - onChange: _propTypes.default.func, - - /** Callback function that is called when a new chip was removed (in controlled mode). */ - onDelete: _propTypes.default.func, - - /** Callback function that is called when the input changes. */ - onUpdateInput: _propTypes.default.func, - - /** A placeholder that is displayed if the input has no values. */ - placeholder: _propTypes.default.string, - - /** Makes the chip input read-only if set to true. */ - readOnly: _propTypes.default.bool, - - /** The chips to display (enables controlled mode if set). */ - value: _propTypes.default.array, - - /** The variant of the Input component */ - variant: _propTypes.default.oneOf(['outlined', 'standard', 'filled']), -} -ChipInput.defaultProps = { - allowDuplicates: false, - blurBehavior: 'clear', - clearInputValueOnChange: false, - delayBeforeAdd: false, - disableUnderline: false, - newChipKeyCodes: [13], - newChipKeys: ['Enter'], - variant: 'standard', -} - -var _default = (0, _withStyles.default)(styles, { name: 'WAMuiChipInput' })(ChipInput) - -const __default = _default -export { __default as default } - -var defaultChipRenderer = function defaultChipRenderer(_ref, key) { - var text = _ref.text, - isFocused = _ref.isFocused, - isDisabled = _ref.isDisabled, - isReadOnly = _ref.isReadOnly, - handleClick = _ref.handleClick, - handleDelete = _ref.handleDelete, - className = _ref.className - return _react.default.createElement(_Chip.default, { - key: key, - className: className, - style: { - pointerEvents: isDisabled || isReadOnly ? 'none' : undefined, - backgroundColor: isFocused ? _blue.default[300] : undefined, - }, - onClick: handleClick, - onDelete: handleDelete, - label: text, - }) -} - -const _defaultChipRenderer = defaultChipRenderer -export { _defaultChipRenderer as defaultChipRenderer } diff --git a/client/src/components/CreatePostDialog.jsx b/client/src/components/CreatePostDialog.jsx new file mode 100644 index 00000000..fe8269e8 --- /dev/null +++ b/client/src/components/CreatePostDialog.jsx @@ -0,0 +1,13 @@ +import { Dialog } from '@mui/material' +import { CreatePost } from './Forms' + +const CreatePostDialog = ({ open, setOpen }) => { + const handleClose = () => setOpen(false) + + return ( + + + + ) +} +export default CreatePostDialog diff --git a/client/src/components/DialogBox/DeleteDialogBox.jsx b/client/src/components/DialogBox/DeleteDialogBox.jsx deleted file mode 100644 index 3d832988..00000000 --- a/client/src/components/DialogBox/DeleteDialogBox.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { DialogActions, DialogContent, DialogContentText, Button, Dialog, DialogTitle, ButtonGroup, CircularProgress } from '@mui/material' -import { useSelector } from 'react-redux' - -const DeleteDialogBox = (props) => { - const { isDeletingPost } = useSelector((state) => state.posts) // [] -> { isLoading, posts: [] } - const { open, setOpen, post, callBack } = props - const handleClose = () => { - setOpen(false) - } - - return ( - - Move your post to bin? - - - Post with postId {post._id} will be deleted permanently. This is an irreversible action and the post can not be recovered again. All likes, comments, associated with this post will also be removed from our database. - - - - - - - - - - ) -} - -export default DeleteDialogBox diff --git a/client/src/components/Form/FileInput/FileInput.jsx b/client/src/components/Form/FileInput/FileInput.jsx deleted file mode 100644 index 2f4dfd45..00000000 --- a/client/src/components/Form/FileInput/FileInput.jsx +++ /dev/null @@ -1,52 +0,0 @@ -import imageCompression from 'browser-image-compression' -import { Button, Collapse } from '@mui/material' -import DeleteIcon from '@mui/icons-material/Delete' -import { Root, classes } from './styles' - -export const compress = async (postData, setPostData, setFileName, setMedia, e) => { - const imageFile = e.dataTransfer?.files[0] || e.target?.files[0] - setMedia(null) - try { - console.log(`Original File Size ${imageFile.size / (1024 * 1024)} Mb`) - const compressedFile = await imageCompression(imageFile, { maxSizeMB: 1, maxWidthOrHeight: 1920 }) - const base64 = await imageCompression.getDataUrlFromFile(compressedFile) - - console.log('Image Compressed', Math.round((base64.length - 'data:image/jpeg;base64,'.length) * 0.75) / 1024 /1024, 'Mb') - const thumbnailFile = await imageCompression(imageFile, { maxSizeMB: 100 / 1024, maxWidthOrHeight: 800 }) - const thumbnail = await imageCompression.getDataUrlFromFile(thumbnailFile) - console.log('Thumbnail', Math.round((thumbnail.length - 'data:image/jpeg;base64,'.length) * 0.75) / 1024, 'KB') - - setPostData({ ...postData, image: base64, thumbnail }) - setMedia(base64) - setFileName(imageFile.name) - } catch (error) { - alert(error.message) - setFileName('No post selected') - } -} - -const defaultMedia = 'https://user-images.githubusercontent.com/194400/49531010-48dad180-f8b1-11e8-8d89-1e61320e1d82.png' -export const FileInput = ({ postData, setPostData, fileName, setFileName, media, setMedia, setEmpty }) => { - return ( - -
- compress(postData, setPostData, setFileName, setMedia, e)} style={{ display: 'none' }} id="raised-button-file" multiple={false} /> - -
-

{fileName}

-
-
- - {fileName} - - - -
- ) -} diff --git a/client/src/components/Form/FileInput/styles.js b/client/src/components/Form/FileInput/styles.js deleted file mode 100644 index a2eaa4df..00000000 --- a/client/src/components/Form/FileInput/styles.js +++ /dev/null @@ -1,55 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'FileInput' - -export const classes = { - root: `${PREFIX}-root`, - fileInput: `${PREFIX}-fileInput`, - inputButton: `${PREFIX}-fileInputButton`, - fileName: `${PREFIX}-fileName`, - media: `${PREFIX}-media`, -} - -export const Root = styled('div')({ - [`&.${classes.root}`]: { - width: '-webkit-fill-available', - margin: '4px 0', - }, - [`& .${classes.fileName}`]: { - position: 'relative', - width: '-webkit-fill-available', - p: { - whiteSpace: 'nowrap', - overflow: 'hidden', - paddingRight: '0px', - textOverflow: 'ellipsis', - position: 'absolute', - width: 'inherit', - bottom: '-22px', - }, - }, - [`& .${classes.fileInput}`]: { - width: '100%', - display: 'flex', - alignItems: 'center', - color: 'white', - justifyContent: 'flex-start', - }, - [`& .${classes.inputButton}`]: { - background: '#ffffff63', - color: 'white', - margin: '0 10px 0px 0', - paddingBottom: '1px', - align: 'center', - width: 'max-content', - }, - [`& .${classes.media}`]: { - marginTop: '5px', - borderRadius: '5px', - objectFit: 'cover', - width: '100%', - maxHeight: '239px', - aspectRatio: '1.77', - position: 'block' - }, -}) diff --git a/client/src/components/Form/Form.jsx b/client/src/components/Form/Form.jsx deleted file mode 100644 index 1f6f963f..00000000 --- a/client/src/components/Form/Form.jsx +++ /dev/null @@ -1,221 +0,0 @@ -import { useState, useEffect, useContext } from 'react' -import { TextField, Typography, Paper, Button, CircularProgress } from '@mui/material' - - -import Autocomplete from '@mui/material/Autocomplete'; -import { MuiChipsInput } from 'mui-chips-input' -import { useLocation, useNavigate } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' -import { Root, classes } from './styles' -import { createPost, updatePost } from '../../actions/posts' -import { compress, FileInput } from './FileInput/FileInput' -import PrivateSwitch from './PrivateSwitch/PrivateSwitch' -import { SnackbarContext } from '../../contexts/SnackbarContext' -import { ModeContext } from '../../contexts/ModeContext' -import * as api from '../../api/index' -const useQuery = () => new URLSearchParams(useLocation().search) - -const initial = { title: '', message: '', image: null, tags: [], private: false } - -const Form = ({ currentId, setCurrentId, user }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const [postData, setPostData] = useState(initial) - const [private_, setPrivate] = useState(postData.private) - const [tags, setTags] = useState(postData.tags) - const [fileName, setFileName] = useState('No post selected') - const [oldLabel, setOldLabel] = useState(fileName) - const [dragging, setDragging] = useState(false) - const [media, setMedia] = useState(postData.image) - const post = useSelector((state) => (currentId ? state.posts.posts.find((p) => p._id === currentId) : null)) - const validate = !(postData.title.trim() && postData.message.trim() && media) - const { isCreatingPost } = useSelector((state) => state.posts) - const dispatch = useDispatch() - const history = useNavigate() - const query = useQuery() - const page = query.get('page') ?? 1 - const { posts, isLoading } = useSelector((state) => state.posts) - - - const[statictags,Setstatictags]=useState([]); - - // const [selecttags,Setselecttags]=useState(''); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await api.fetchTags(); - - Setstatictags(response.data.data); - } catch (error) { - console.error('Error fetching tags:', error); - // Handle the error as needed, e.g., show a message to the user - } - }; - - fetchData(); - - - }, []); - - - - const handleChange = (newtags) => { - setTags(newtags) - setPostData({ ...postData, tags: newtags }) - } - - const { mode } = useContext(ModeContext) - - useEffect(() => { - if (post) { - setPostData(post) - setPrivate(post.private) - setTags(post.tags) - setFileName('Previous Image') - setOldLabel('Previous Image') - setMedia(post.thumbnail) - } - }, [post]) - - const dragEnter = (e) => { - e.preventDefault() - setFileName('Uploading File') - } - - const fileDrop = (e) => { - e.preventDefault() - compress(postData, setPostData, setFileName, setMedia, e) - } - - const dragLeave = (e) => { - e.preventDefault() - setFileName(oldLabel) - } - - useEffect(() => { - if (dragEnter) setDragging(true) - setDragging(false) - }, [setDragging]) - - const setMediaEmpty = () => { - setMedia(null) - setFileName('No post selected') - } - const clear = () => { - setCurrentId(0) - setTags([]) - setPostData(initial) - setPrivate(initial.private) - setMediaEmpty() - } - - useEffect(() => { - clear() - }, [page]) - const handleSubmit = (e) => { - e.preventDefault() - postData.tags = postData.tags.map((tag) => - tag - .toLowerCase() - .trim() - .replace(/[^a-zA-Z0-9 ]/g, '') - ) - const userId = user.result._id || user.result.googleId.padStart(24, '0') - if (currentId === 0) { - dispatch(createPost({ ...postData, creator: userId }, history, snackBar, clear)) - } else { - dispatch(updatePost(currentId, postData, snackBar, clear)) - } - } - - // const handleAdd = (tag) => { - // const array = [...tags, tag] - // setTags(array) - // setPostData({ ...postData, tags: array }) - // } - // const handleDelete = (tagToDelete) => { - // const array = tags.filter((tag) => tag !== tagToDelete) - // setTags(array) - // setPostData({ ...postData, tags: array }) - // } - - if (!user?.result?.name) { - return ( - - - - Please Sign In to create your memories with us and like other's memories. - - - - ) - } - return ( - - -
- - {currentId ? `Editing ${post.title}` : 'Create a Memory'} - - - - setPostData({ ...postData, title: e.target.value })} /> - setPostData({ ...postData, message: e.target.value })} /> - {/* */} - {/* */} - { - setTags(newTags); - - const updatedSelectTags = Array.from(statictags).filter(tag => !newTags.includes(tag)); - - - Setstatictags(updatedSelectTags); - setPostData({ ...postData, tags: newTags }); - } - } - renderInput={(params) => ( - - )} - /> - - - -
-
- ) -} - -export default Form diff --git a/client/src/components/Form/PrivateSwitch/PrivateSwitch.jsx b/client/src/components/Form/PrivateSwitch/PrivateSwitch.jsx deleted file mode 100644 index da004d61..00000000 --- a/client/src/components/Form/PrivateSwitch/PrivateSwitch.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Root, classes } from './styles' -import { Typography } from '@mui/material' -import { ModeContext } from '../../../contexts/ModeContext' -import { useContext } from 'react' - -const PrivateSwitch = ({ private_, postData, setPrivate, setPostData }) => { - const onChange = () => setPostData({ ...postData, private: !private_ }) - const { mode } = useContext(ModeContext) - - return ( - - - - Private - - - ) -} - -export default PrivateSwitch diff --git a/client/src/components/Form/PrivateSwitch/styles.js b/client/src/components/Form/PrivateSwitch/styles.js deleted file mode 100644 index 46bb0e97..00000000 --- a/client/src/components/Form/PrivateSwitch/styles.js +++ /dev/null @@ -1,99 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'privateSwitch' - -export const classes = { - root: `${PREFIX}-root`, - i: `${PREFIX}-i`, - formSwitch: `${PREFIX}-formSwitch`, - input: `${PREFIX}-input`, - darkMode: `${PREFIX}-darkMode`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - width: '-webkit-fill-available', - display: 'flex', - alignItems: 'center', - marginBottom: '3px', - }, - [`& .${classes.formSwitch}`]: { - display: 'flex', - cursor: 'pointer', - alignItems: 'center', - ':active i::after': { - width: '28px', - transform: 'translate3d(2px, 2px, 0)', - }, - ':active input:checked + i::after': { - transform: 'translate3d(16px, 2px, 0)', - }, - }, - - [`& .${classes.input}`]: { - display: 'none', - ':checked + i': { - backgroundColor: '#1976d28c', - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundImage: `url('data:image/svg+xml;utf8,')`, - }, - ':checked + i::before': { - transform: 'translate3d(-3px, 2px, 0) scale3d(0, 0, 0)', - }, - ':checked + i::after': { - transform: 'translate3d(23px, 2px, 0)', - }, - }, - - [`& .${classes.i}`]: { - position: 'relative', - display: 'inline-block', - marginRight: '.5rem', - width: '52px', - height: '25px', - backgroundRepeat: 'no-repeat', - backgroundImage: `url('data:image/svg+xml;utf8,')`, - borderRadius: '25px', - verticalAlign: 'text-bottom', - transition: 'all 0.3s', - '::before': { - content: '""', - position: 'absolute', - left: '-2px', - top: '-3px', - width: '52px', - height: '25px', - borderStyle: 'solid', - borderWidth: '1px', - borderRadius: '25px', - transform: 'translate3d(2px, 2px, 0) scale3d(1, 1, 1)', - transition: 'all 0.3s', - }, - '::after': { - content: '""', - position: 'absolute', - left: '6px', - top: '0.35px', - width: '20px', - height: '20px', - backgroundColor: 'black', - borderRadius: '25px', - boxShadow: '0 2px 2px rgba(0, 0, 0, 0.24)', - transform: 'translate3d(-3px, 2px, 0)', - transition: 'all 0.2s ease-in-out', - }, - }, - - [`& .${classes.darkMode}`]: { - backgroundImage: `url('data:image/svg+xml;utf8,')`, - [`& .${classes.i}`]: { - '::before': { - color: 'white', - }, - '::after': { - backgroundColor: 'white', - } - } - } -})) diff --git a/client/src/components/Form/styles.js b/client/src/components/Form/styles.js deleted file mode 100644 index 3345c98d..00000000 --- a/client/src/components/Form/styles.js +++ /dev/null @@ -1,66 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Form' - -export const classes = { - root: `${PREFIX}-root`, - paperLight: `${PREFIX}-paperLight`, - paperDark: `${PREFIX}-paperDark`, - form: `${PREFIX}-form`, - buttonSubmit: `${PREFIX}-buttonSubmit`, - chip: `${PREFIX}-chip`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - '& .MuiTextField-root': { - margin: theme.spacing(0.5, 0) - }, - '& .MuiOutlinedInput-root': { - color: 'white', - }, - '& .MuiFormLabel-root': { - color: 'white', - }, - '& .MuiChip-filled': { - background: '#ffffff70', - }, - }, - [`& .${classes.paperLight}`]: { - padding: theme.spacing(2), - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - - [`& .${classes.paperDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .90)', - color: 'white', - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: '#b3b3b3', - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - }, - }, - [`& .${classes.buttonSubmit}`]: { - '&.Mui-disabled': { - backgroundColor: '#aebfd1' - }, - }, - }, - [`& .${classes.form}`]: { - display: 'flex', - flexWrap: 'wrap', - justifyContent: 'center', - }, - [`& .${classes.buttonSubmit}`]: { - marginTop: 10, - }, - [`& .${classes.chip}`]: { - margin: '5px 0', - }, -})) - -export default Root diff --git a/client/src/components/Forms/CreateComment.jsx b/client/src/components/Forms/CreateComment.jsx new file mode 100644 index 00000000..ae0f3585 --- /dev/null +++ b/client/src/components/Forms/CreateComment.jsx @@ -0,0 +1,5 @@ +const CreateComment = () => { + return
CreateComment
+} + +export default CreateComment diff --git a/client/src/components/Forms/CreatePost.jsx b/client/src/components/Forms/CreatePost.jsx new file mode 100644 index 00000000..2164eec8 --- /dev/null +++ b/client/src/components/Forms/CreatePost.jsx @@ -0,0 +1,237 @@ +import { Autocomplete, Box, Button, ButtonGroup, CircularProgress, Collapse, FormControl, FormHelperText, IconButton, Input, InputAdornment, Paper, Stack, TextField, Tooltip, Typography } from '@mui/material' +import { movies } from '@/data' +import { AddAPhotoOutlined, AutoAwesome, Close, Visibility, VisibilityOff } from '@mui/icons-material' +import { useState } from 'react' +import { useAuth } from '@clerk/clerk-react' +import { Link } from 'react-router-dom' +import { useCreatePost } from '@/hooks' +import { convertToBase64 } from '@/lib/utils' +import { useStore } from '@/store' + +const Form = () => { + const { isLoaded } = useAuth() + const { mutate: createPost, isLoading } = useCreatePost() + const initialData = { title: '', description: '', tags: [], visibility: 'PUBLIC', media: null } + const initialErrorState = { title: '', description: '', tags: '', media: '' } + const { openSnackbar } = useStore() + const [formData, setFormData] = useState(initialData) + const [errors, setErrors] = useState(initialErrorState) + const [preview, setPreview] = useState(null) + const handleChange = (event) => { + setErrors(initialErrorState) + + const { name, value, files } = event.target + if (name === 'title' && value.length > 30) { + setErrors({ ...errors, title: 'Title must be less than 30 characters' }) + } else if (name === 'visibility') { + setFormData({ ...formData, visibility: formData.visibility === 'PUBLIC' ? 'PRIVATE' : 'PUBLIC' }) + } else if (name === 'description' && value.length > 150) { + setErrors({ ...errors, description: 'Description must be less than 150 characters' }) + } else if (name === 'media' && files && files[0]) { + const file = files[0] + setFormData({ ...formData, [name]: file }) + const reader = new FileReader() + reader.onloadend = () => { + setPreview(reader.result) + } + reader.readAsDataURL(file) + } else if (name === 'tags') { + setFormData({ ...formData, [name]: value.split(',') }) + } else { + setFormData({ ...formData, [name]: value }) + } + } + + const validateInputs = () => { + const newErrors = { ...initialErrorState } + let valid = true + + if (formData.title.trim() === '') { + newErrors.title = 'Title is required' + valid = false + } + if (formData.description.trim() === '') { + newErrors.description = 'Description is required' + valid = false + } + if (formData.tags.length === 0) { + newErrors.tags = 'At least one tag is required' + valid = false + } + if (!formData.media) { + newErrors.media = 'Please pick an image' + valid = false + } + setErrors(newErrors) + return valid + } + const generateTags = () => { + const aIGenerateTags = ['social', 'media', 'post'] + setFormData({ ...formData, tags: [...new Set([...formData.tags, ...aIGenerateTags].slice(0, 8))] }) + } + const handleClear = () => { + setFormData(initialData) + setPreview(null) + } + const handleSubmit = async (event) => { + event.preventDefault() + setErrors(initialErrorState) + if (!validateInputs()) { + return + } + try { + createPost({ + ...formData, + media: await convertToBase64(formData.media) + }) + openSnackbar('Post created successfully', 'success') + } catch (error) { + console.error(error) + openSnackbar('Post creation failed', 'error') + } finally { + handleClear() + } + } + + const handleSearchInput = (params) => { + return ( + + = 8} edge="end" size="small"> + + + + ) + } + }} + /> + ) + } + + return ( + + + Create a Post + + + + + { + setFormData({ ...formData, media: null }) + setPreview(null) + }} + sx={{ + position: 'absolute', + top: 8, + right: 8, + zIndex: 1, + '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.7)' } + }} + > + + + + + + + + + + + + + + + + {formData.visibility === 'PUBLIC' ? : } + + + + + + {errors.title} + + + + {errors.description} + + + option.title)} + renderInput={handleSearchInput} + value={formData.tags} + onChange={(_, value) => { + setFormData((prevData) => ({ + ...prevData, + tags: value.length > 8 ? value.slice(-8) : value + })) + setErrors({ ...errors, tags: '' }) + }} + onInputChange={(_, value) => (formData.tags.length < 8 ? value : '')} + disableClearable + /> + {errors.tags} + + + {errors.media} + + + + + + + + ) +} + +const CreatePost = () => { + const { isSignedIn } = useAuth() + + return ( + `1px solid ${theme.palette.divider}` }}> + {isSignedIn ? ( +
+ ) : ( + + + Please Sign in to create a post + + + + )} + + ) +} + +export default CreatePost diff --git a/client/src/components/Forms/Search.jsx b/client/src/components/Forms/Search.jsx new file mode 100644 index 00000000..ade5530b --- /dev/null +++ b/client/src/components/Forms/Search.jsx @@ -0,0 +1,28 @@ +import { movies } from '@/data' +import { Autocomplete, Button, FormControl, FormGroup, Paper, Stack, TextField } from '@mui/material' + +const Search = () => { + const handleSearchInput = (params) => + + const handleSubmit = (event) => { + event.preventDefault() + } + + return ( + `1px solid ${theme.palette.divider}` }}> + + + + + + option.title)} renderInput={handleSearchInput} /> + + + + + ) +} + +export default Search diff --git a/client/src/components/Forms/UpdateProfile.jsx b/client/src/components/Forms/UpdateProfile.jsx new file mode 100644 index 00000000..4b7510df --- /dev/null +++ b/client/src/components/Forms/UpdateProfile.jsx @@ -0,0 +1,163 @@ +import { useState } from 'react' +import { useUser } from '@clerk/clerk-react' +import { List, ListItem, DialogTitle, Dialog, TextField, Button, Paper, Avatar, IconButton, Input, Stack, FormControl, FormHelperText, ButtonGroup, Grid2 as Grid, CircularProgress } from '@mui/material' +import { useStore } from '@/store' +import { AddAPhotoOutlined, Close } from '@mui/icons-material' + +const UpdateProfile = ({ open, onClose: handleClose }) => { + const { user } = useUser() + const { openSnackbar } = useStore() + const initialErrorState = { imageUrl: '', firstName: '', lastName: '', bio: '' } + const initialData = { imageUrl: user.imageUrl, firstName: user.firstName, lastName: user.lastName, bio: user.unsafeMetadata.bio || '' } + const [errors, setErrors] = useState(initialErrorState) + const [editedUser, setEditedUser] = useState(initialData) + const [newImage, setNewImage] = useState(null) + const [loading, setLoading] = useState(false) + const handleInputChange = (e) => setEditedUser({ ...editedUser, [e.target.name]: e.target.value }) + + const handleImageChange = (e) => { + if (e.target.files?.[0]) { + const file = e.target.files[0] + setNewImage(file) + setEditedUser({ ...editedUser, imageUrl: URL.createObjectURL(file) }) + } + } + + const validateInputs = () => { + const newErrors = { ...initialErrorState } + let valid = true + + // Name validation + const nameRegex = /^[A-Za-z]+$/ + if (!nameRegex.test(editedUser.firstName)) { + newErrors.firstName = 'Name must contain only letters' + valid = false + } + if (editedUser.firstName.length < 1) { + newErrors.firstName = 'Name must be at least 1 character' + valid = false + } + if (editedUser.lastName && !nameRegex.test(editedUser.lastName)) { + newErrors.lastName = 'Name must contain only letters' + valid = false + } + setErrors(newErrors) + return valid + } + + const handleSubmit = async (event) => { + event.preventDefault() + setErrors(initialErrorState) + if (!validateInputs()) { + return + } + setLoading(true) + try { + // Update user profile + await user.update({ + firstName: editedUser.firstName, + lastName: editedUser.lastName, + unsafeMetadata: { ...user.unsafeMetadata, bio: editedUser.bio } + }) + // Update profile picture if changed + if (newImage) { + await user.setProfileImage({ file: newImage }) + } + handleClose() + openSnackbar('Profile successfully updated 🎊', 'success') + } catch (error) { + console.error('Error updating user:', error.errors[0]?.longMessage || error.message) + openSnackbar(error.errors[0]?.longMessage || error.message, 'error') + } finally { + setLoading(false) + } + } + + const handleReset = () => { + setEditedUser(initialData) + setNewImage(null) + } + + return ( + + + Edit Profile + + + + + + + + + + + + + + + + + + + {errors.firstName} + + + + + + {errors.lastName} + + + + + + + + {errors.bio} + + + + + + {errors.imageUrl} + + + + + + + + + ) +} + +export default UpdateProfile diff --git a/client/src/components/Forms/index.js b/client/src/components/Forms/index.js new file mode 100644 index 00000000..fd3c9430 --- /dev/null +++ b/client/src/components/Forms/index.js @@ -0,0 +1,4 @@ +export { default as CreateComment } from './CreateComment' +export { default as CreatePost } from './CreatePost' +export { default as Search } from './Search' +export { default as UpdateProfile } from './UpdateProfile' diff --git a/client/src/components/Home/index.jsx b/client/src/components/Home/index.jsx deleted file mode 100644 index 41839f04..00000000 --- a/client/src/components/Home/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useState } from 'react' -import { useLocation } from 'react-router-dom' -import { Container, Grow, Grid } from '@mui/material' -import { Root, classes } from './styles' -import Posts from '../Posts' -import Form from '../Form/Form' -import Paginate from '../Paginate/Paginate' -import Search from '../Search/Search' - -const useQuery = () => new URLSearchParams(useLocation().search) - -const Home = ({ user }) => { - const [currentId, setCurrentId] = useState(0) - const query = useQuery() - const page = query.get('page') ?? 1 - const searchQuery = query.get('searchQuery') - const [tags, setTags] = useState([]) - - return ( - - - - - - - - - - - {!searchQuery && } - - - - - - ) -} - -export default Home diff --git a/client/src/components/Home/styles.js b/client/src/components/Home/styles.js deleted file mode 100644 index 169f1626..00000000 --- a/client/src/components/Home/styles.js +++ /dev/null @@ -1,30 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Home' -export const classes = { - appBarSearch: `${PREFIX}-appBarSearch`, - pagination: `${PREFIX}-pagination`, - gridContainer: `${PREFIX}-gridContainer`, - buttonSearch: `${PREFIX}-buttonSearch`, - container: `${PREFIX}-container`, -} -export const Root = styled('div')(({ theme }) => ({ - [`& .${classes.gridContainer}`]: { - justifyContent: 'space-between', - alignItems: 'stretch', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column', - padding: '0 0 0 10px', - }, - }, - [`& .${classes.container}`]: { - margin: '40px 0', - maxWidth: '-webkit-fill-available', - [theme.breakpoints.down(600)]: { - margin: '40px 0', - padding: '0 7px', - }, - }, -})) - -export default Root diff --git a/client/src/components/Input/index.jsx b/client/src/components/Input/index.jsx deleted file mode 100644 index 9c00d8ff..00000000 --- a/client/src/components/Input/index.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { TextField, Grid, InputAdornment, IconButton, AppBar } from '@mui/material' -import { Visibility, VisibilityOff } from '@mui/icons-material' -import { Root, classes } from './styles' -import { ModeContext } from '../../contexts/ModeContext' -import { useContext } from 'react' -const Input = ({ name, half, type, handleShowPassword, ...props }) => { - const { mode } = useContext(ModeContext) - return ( - - - - - {type === 'password' ? : } - - ), - } - : null, - { style: { color: 'white' } }) - } - /> - - - - ) -} - -export default Input diff --git a/client/src/components/Input/styles.js b/client/src/components/Input/styles.js deleted file mode 100644 index 34d6f780..00000000 --- a/client/src/components/Input/styles.js +++ /dev/null @@ -1,38 +0,0 @@ -import { styled } from "@mui/material/styles" - -const PREFIX = "Search" -export const classes = { - root: `${PREFIX}-root`, - searchBarLight: `${PREFIX}-searchBarLight`, - searchBarDark: `${PREFIX}-searchBarDark`, - searchButton: `${PREFIX}-searchButton`, - chip: `${PREFIX}-chip`, -} - -export const Root = styled("div")(({ theme }) => ({ - [`& .${classes.searchBarLight}`]: { - backgroundColor: "transparent", - boxShadow: "none", - '& .MuiOutlinedInput-root': { - '& fieldset': { - // borderColor: '#000000', - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - }, - }, - }, - [`& .${classes.searchBarDark}`]: { - backgroundColor: "rgba(5, 5, 5, .90)", - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: '#b3b3b3', - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - }, - }, - }, -})) - -export default Root diff --git a/client/src/components/Navbar.jsx b/client/src/components/Navbar.jsx new file mode 100644 index 00000000..d2a1358a --- /dev/null +++ b/client/src/components/Navbar.jsx @@ -0,0 +1,226 @@ +import { useState, useEffect } from 'react' +import { AppBar, Box, Toolbar, IconButton, Container, Button, ButtonGroup, Stack, useTheme, useMediaQuery, Dialog, DialogContent, TextField, Autocomplete, InputAdornment, Paper, Divider, Avatar } from '@mui/material' +import { Link, useLocation, useNavigate } from 'react-router-dom' +import { useAuth } from '@clerk/clerk-react' +import { Menu, Search, LoginOutlined, GitHub } from '@mui/icons-material' +import { AccountMenu, ThemeSwitch, Sidebar } from '@/components' +import { brand } from '@/assets' + +// Mock data for search suggestions +const searchSuggestions = [ + { title: 'Home', url: '/' }, + { title: 'About', url: '/about' }, + { title: 'Products', url: '/products' }, + { title: 'Contact', url: '/contact' } + // Add more suggestions as needed +] + +const Branding = () => { + return ( + + + theme.palette.mode === 'light' && 'invert(1)' }} /> + + ) +} + +const LoggedOutOptions = () => { + const { pathname } = useLocation() + const { isLoaded, isSignedIn } = useAuth() + const inAuth = ['/login', '/signup', '/verify-email'].includes(pathname) + + if (!isLoaded || inAuth || isSignedIn) { + return null + } + + return ( + + + theme.palette.primary.main + }} + > + + + + ) +} + +const SearchBar = ({ onFocus: handleFocus }) => { + return ( + + + + ), + endAdornment: ( + + + + ) + } + }} + onClick={handleFocus} + variant="outlined" + /> + ) +} + +const SearchDialog = ({ open, onClose: closeBox }) => { + const [searchTerm, setSearchTerm] = useState('') + const navigate = useNavigate() + + const handleClose = () => { + setSearchTerm('') + closeBox() + } + const handleSearch = (_, value) => { + if (value) { + navigate(value.url) + handleClose() + } + } + + return ( + + + + option.title} + renderInput={(params) => ( + + + + ) + } + }} + /> + )} + inputValue={searchTerm} + onInputChange={(_, newValue) => setSearchTerm(newValue)} + onChange={handleSearch} + /> + + + + ) +} + +const Navbar = () => { + const [open, setOpen] = useState(false) + const [searchOpen, setSearchOpen] = useState(false) + const handleOpen = () => setOpen(!open) + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + + useEffect(() => { + const handleKeyDown = (event) => { + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault() + setSearchOpen(true) + } + } + + document.addEventListener('keydown', handleKeyDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + } + }, []) + + return ( + + + + + + + {isMobile ? ( + setSearchOpen(true)} edge="end"> + + + ) : ( + setSearchOpen(true)} /> + )} + {isMobile ? ( + + + + ) : ( + + + + + + + + + + + )} + + + + {isMobile && open && ( + + + + + + + )} + + + setSearchOpen(false)} /> + + ) +} + +export default Navbar diff --git a/client/src/components/Navbar/FloatingNavbar.jsx b/client/src/components/Navbar/FloatingNavbar.jsx deleted file mode 100644 index 3f1703dc..00000000 --- a/client/src/components/Navbar/FloatingNavbar.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { AppBar } from '@mui/material' -import { useState, useEffect } from 'react' -import Navbar from './Navbar' - -const FloatingNavbar = ({ user, setUser }) => { - const [isVisible, setIsVisible] = useState(false) - - useEffect(() => { - // Button is displayed after scrolling for 200 pixels - const toggleVisibility = () => setIsVisible(window.scrollY > 130) - window.addEventListener('scroll', toggleVisibility) - return () => window.removeEventListener('scroll', toggleVisibility) - }, []) - return ( - - - - ) -} - -export default FloatingNavbar diff --git a/client/src/components/Navbar/Navbar.jsx b/client/src/components/Navbar/Navbar.jsx deleted file mode 100644 index 82c4259b..00000000 --- a/client/src/components/Navbar/Navbar.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useEffect, useCallback, useContext } from 'react' -import { Link, useNavigate, useLocation } from 'react-router-dom' -import { AppBar, Typography, Toolbar, Button, Avatar } from '@mui/material' -import { useDispatch } from 'react-redux' -import { Root, classes } from './styles' -import memories from '../../images/memories.png' -import icon from '../../images/icon.png' -import { jwtDecode } from 'jwt-decode' -import Avaatar from 'avataaars2' -import { SnackbarContext } from '../../contexts/SnackbarContext' -import { ModeContext } from '../../contexts/ModeContext' - -const Navbar = ({ user, setUser, floating }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const token = user?.token - const dispatch = useDispatch() - const history = useNavigate() - const location = useLocation() - const userIsinAuth = location.pathname === '/auth' - - const logout = useCallback(() => { - dispatch({ type: 'LOGOUT' }) - history('/') - snackBar('info', 'Logged out') - setUser(null) - }, [history, dispatch, setUser]) - - useEffect(() => { - if (token) { - const decodedToken = jwtDecode(token) - if (decodedToken.exp * 1000 < new Date().getTime()) { - logout() - } - } - setUser(JSON.parse(localStorage.getItem('profile'))) - }, [logout, token]) - - const { mode, modeToggle } = useContext(ModeContext) - - return ( - - - - memories - memories - - - {user ? ( -
- - {user.result.avatar ? ( - - ) : ( - - {user.result.name.charAt(0)} - - )} - - - {user.result.name} - - -
- ) : ( - !userIsinAuth && ( - - ) - )} - -
-
-
- ) -} - -export default Navbar diff --git a/client/src/components/Navbar/styles.js b/client/src/components/Navbar/styles.js deleted file mode 100644 index 5d88d58d..00000000 --- a/client/src/components/Navbar/styles.js +++ /dev/null @@ -1,185 +0,0 @@ -import { styled } from '@mui/material/styles'; - -import lightmodeIcon from '../../images/lightmodeIcon.png'; -import darkmodeIcon from '../../images/darkmodeIcon.png'; - -const PREFIX = 'Navbar'; -export const classes = { - root: `${PREFIX}-root`, - appBarLight: `${PREFIX}-appBarLight`, - appBarDark: `${PREFIX}-appBarDark`, - heading: `${PREFIX}-heading`, - logo: `${PREFIX}-logo`, - toolbar: `${PREFIX}-toolbar`, - profile: `${PREFIX}-profile`, - authButtonLight: `${PREFIX}-authButtonLight`, - authButtonDark: `${PREFIX}-authButtonDark`, - userName: `${PREFIX}-userName`, - brandContainer: `${PREFIX}-brandContainer`, - avatar: `${PREFIX}-avatar`, - avaatar: `${PREFIX}-avaatar`, - toggleDiv: `${PREFIX}-toggleDiv`, - dn: `${PREFIX}-dn`, - toggle: `${PREFIX}-toggle`, -}; - -export const Root = styled('div')(({ theme, floating }) => ({ - [`&.${classes.root}`]: { - paddingRight: 10, - }, - [`& .${classes.avaatar}`]: { - margin: theme.spacing(1), - height: '50px', - width: '50px', - }, - [`& .${classes.appBarLight}`]: { - position: 'static', - borderRadius: '5px', - margin: '0 16px 0 6px', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: '10px 50px', - backgroundColor: 'rgba(255, 255, 255, 0.09)', - backdropFilter: 'blur(10px)', - [theme.breakpoints.down('md')]: { - flexDirection: 'column', - }, - [theme.breakpoints.down(360)]: { - padding: '10px', - }, - }, - [`& .${classes.appBarDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .90)', - }, - [`& .${classes.heading}`]: { - height: floating ? 50 : 100, - [theme.breakpoints.down(400)]: { - width: '-webkit-fill-available', - }, - }, - [`& .${classes.logo}`]: { - marginLeft: '10px', - marginTop: '5px', - height: floating ? '30px' : '60px', - [theme.breakpoints.down(400)]: { - display: 'none', - }, - }, - [`& .${classes.toolbar}`]: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - gap: '30px', - [theme.breakpoints.down('md')]: { - width: '-webkit-fill-available', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - gap: '40px', - }, - [theme.breakpoints.down(600)]: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - gap: '20px', - }, - }, - [`& .${classes.profile}`]: { - display: 'flex', - justifyContent: 'space-between', - width: '400px', - alignItems: 'center', - gap: '20px', - [theme.breakpoints.down(920)]: { - gap: 10, - width: 350 - }, - [theme.breakpoints.down(600)]: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - }, - [theme.breakpoints.down(390)]: { - width: '-webkit-fill-available', - justifyContent: 'space-evenly', - }, - }, - [`& .${classes.authButtonLight}`]: { - backgroundColor: 'black', - }, - [`& .${classes.authButtonDark}`]: { - backgroundColor: 'white', - color: 'black', - }, - [`& .${classes.userName}`]: { - display: 'flex', - textAlign: 'center', - alignItems: 'center', - [theme.breakpoints.down(420)]: { - display: 'none', - }, - }, - [`& .${classes.brandContainer}`]: { - display: 'flex', - alignItems: 'center', - }, - [`& .${classes.toggleDiv}`]: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - - // Additional styles for the checkbox input (HiddenCheckbox) - - }, - - [`& .${classes.dn}`]: { - width: '0', - height: '0', - visibility: 'hidden', - // Styles for the label when the checkbox is checked - - ':checked + i': { - backgroundColor: '#242424', - border: 'none', - }, - - ':checked + i::after': { - content: '""', - width: '30px', - height: '30px', - position: 'absolute', - backgroundImage: `url(${darkmodeIcon})`, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundSize: '35px 35px', - transition: '0.8s', - transform: 'translate(100%)', - }, - - // Additional styles for the label (ToggleLabel) - }, - [`& .${classes.toggle}`]: { - width: '60px', - height: '30px', - padding: '4px', - position: 'relative', - display: 'block', - border: '1px solid white', - borderRadius: '200px', - cursor: 'pointer', - transition: '0.8s', - '::after': { - content: '""', - width: '30px', - height: '30px', - position: 'absolute', - backgroundImage: `url(${lightmodeIcon})`, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center', - backgroundSize: '35px 35px', - transition: '0.8s', - }, - } -})); diff --git a/client/src/components/OAuthButtons.jsx b/client/src/components/OAuthButtons.jsx new file mode 100644 index 00000000..d6625367 --- /dev/null +++ b/client/src/components/OAuthButtons.jsx @@ -0,0 +1,36 @@ +import { useSignIn } from '@clerk/clerk-react' +import { GitHub, Google } from '@mui/icons-material' +import { Button, ButtonGroup, FormHelperText } from '@mui/material' +import { useState } from 'react' + +const OAuthButtons = () => { + const [error, setError] = useState('') + const { isLoaded, signIn } = useSignIn() + + const handleOAuthSignIn = async (strategy) => { + if (!isLoaded) { + return + } + try { + await signIn.authenticateWithRedirect({ strategy, redirectUrl: '/callback', redirectUrlComplete: '/' }) + } catch (err) { + setError(err.errors[0].longMessage || 'An error occurred during OAuth sign-in') + } + } + + return ( + + + {error} + + + + + ) +} + +export default OAuthButtons diff --git a/client/src/components/Paginate/Paginate.jsx b/client/src/components/Paginate/Paginate.jsx deleted file mode 100644 index 5d6e7fd9..00000000 --- a/client/src/components/Paginate/Paginate.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useContext, useEffect } from 'react' -import { Root, classes } from './styles' -import { Pagination, PaginationItem, Paper, CircularProgress } from '@mui/material' -import { Link } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' -import { getPosts } from '../../actions/posts' -import { SnackbarContext } from '../../contexts/SnackbarContext' -import { ModeContext } from '../../contexts/ModeContext' -CircularProgress -const Paginate = ({ page }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const { isLoading } = useSelector((state) => state.posts) // [] -> { isLoading, posts: [] } - - const dispatch = useDispatch() - const { numberOfPages } = useSelector((state) => state.posts) - - useEffect(() => { - const fetchPosts = async () => dispatch(getPosts(page, snackBar)) - if (page) fetchPosts() - }, [dispatch, page]) - - const { mode } = useContext(ModeContext) - - return ( - - - {isLoading ? ( -
- -
- ) : ( - } /> - )} -
-
- ) -} - -export default Paginate diff --git a/client/src/components/Paginate/styles.js b/client/src/components/Paginate/styles.js deleted file mode 100644 index 3315e9ca..00000000 --- a/client/src/components/Paginate/styles.js +++ /dev/null @@ -1,54 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Paginate' - -export const classes = { - root: `${PREFIX}-root`, - ul: `${PREFIX}-ul`, - pagination: `${PREFIX}-pagination`, - paperLight: `${PREFIX}-paperLight`, - paperDark: `${PREFIX}-paperDark`, - eachPage: `${PREFIX}-eachPage`, -} - -export const Root = styled('div')({ - [`& .${classes.root}`]: { - '& .Mui-disabled': { - color: 'white', - }, - '&. MuiButtonBase-root-MuiPaginationItem-root': { - border: '1px solid white', - color: 'white', - }, - }, - [`& .${classes.ul}`]: { - justifyContent: 'space-around', - color: 'white', - opacity: 1, - '& .Mui-disabled ': { - color: 'white', - opacity: 0.5, - }, - }, - [`& .${classes.paperLight}`]: { - borderRadius: 4, - marginTop: '1rem', - padding: '16px', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - }, - [`& .${classes.paperDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .90)', - [`& .${classes.eachPage}`]: { - color: 'white', - border: '1px solid white', - }, - }, - [`& .${classes.centerDiv}`]: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - }, -}) - -export default Root diff --git a/client/src/components/PostCard.jsx b/client/src/components/PostCard.jsx new file mode 100644 index 00000000..6801f771 --- /dev/null +++ b/client/src/components/PostCard.jsx @@ -0,0 +1,340 @@ +import { useRef, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { Card, CardHeader, CardMedia, CardContent, CardActions, IconButton, Typography, Button, Popover, Paper, Stack, Box, Fade, CircularProgress, TextField, Autocomplete, Input, Tooltip } from '@mui/material' +import { ThumbUp, Delete, Favorite, EmojiEmotions, SentimentVeryDissatisfied, Mood, SentimentDissatisfied, ThumbUpOutlined, Edit, Cancel, Save, Refresh, VisibilityOff, Visibility, Lock } from '@mui/icons-material' +import { UserAvatar } from '.' +import moment from 'moment' +import { getThumbnail, convertToBase64 } from '@/lib/utils' +import { useUser } from '@clerk/clerk-react' +import { useDeletePost, useUpdatePost } from '@/hooks' + +const reactions = [ + { icon: ThumbUp, label: 'Like', color: '#2196f3' }, + { icon: Favorite, label: 'Love', color: '#e91e63' }, + { icon: EmojiEmotions, label: 'Haha', color: '#ffc107' }, + { icon: SentimentVeryDissatisfied, label: 'Sad', color: '#607d8b' }, + { icon: Mood, label: 'Wow', color: '#4caf50' }, + { icon: SentimentDissatisfied, label: 'Angry', color: '#ff5722' } +] + +const PostCard = ({ post }) => { + const { user } = useUser() + const [editing, setEditing] = useState(false) + const [editedPost, setEditedPost] = useState(post) + const initialErrors = { title: '', description: '', tags: '', media: '' } + const { mutate: deletePost, isPending: isDeleting } = useDeletePost() + const { mutate: updatePost, isPending: isUpdating } = useUpdatePost() + + const [reactionAnchorEl, setReactionAnchorEl] = useState(null) + const [currentReaction, setCurrentReaction] = useState(post.reactions[0]?.reactionType) + + const [errors, setErrors] = useState(initialErrors) + + const popoverTimeoutRef = useRef(null) + const navigate = useNavigate() + + const handleReactionIconEnter = (event) => { + if (!user) { + return + } + clearTimeout(popoverTimeoutRef.current) + setReactionAnchorEl(event.currentTarget) + } + + const handleReactionIconLeave = () => + (popoverTimeoutRef.current = setTimeout(() => { + setReactionAnchorEl(null) + }, 1000)) + + const handlePopoverEnter = () => clearTimeout(popoverTimeoutRef.current) + + const handlePopoverLeave = () => + (popoverTimeoutRef.current = setTimeout(() => { + setReactionAnchorEl(null) + }, 300)) + + const handleReactionSelect = (reaction) => { + setCurrentReaction(reaction === currentReaction ? null : reaction) + setReactionAnchorEl(null) + } + + const handleChange = (event) => { + const { name, value, files } = event.target + setErrors({ ...errors, [name]: '' }) + + if (name === 'title' && value.length > 30) { + setErrors({ ...errors, title: 'Title must be less than 30 characters' }) + } else if (name === 'visibility') { + setEditedPost({ ...editedPost, visibility: value === 'PUBLIC' ? 'PRIVATE' : 'PUBLIC' }) + } else if (name === 'description' && value.length > 150) { + setErrors({ ...errors, description: 'Description must be less than 150 characters' }) + } else if (name === 'media' && files && files[0]) { + const file = files[0] + setEditedPost({ ...editedPost, [name]: file }) + const reader = new FileReader() + reader.onloadend = () => { + setEditedPost((prev) => ({ ...prev, imageUrl: reader.result })) + } + reader.readAsDataURL(file) + } else { + setEditedPost({ ...editedPost, [name]: value }) + } + } + + const validateInputs = () => { + const newErrors = { title: '', description: '', tags: '', media: '' } + let valid = true + + if (editedPost.title.trim() === '') { + newErrors.title = 'Title is required' + valid = false + } + if (editedPost.description.trim() === '') { + newErrors.description = 'Description is required' + valid = false + } + if (editedPost.tags.length === 0) { + newErrors.tags = 'At least one tag is required' + valid = false + } + if (!editedPost.imageUrl) { + newErrors.media = 'Please pick an image' + valid = false + } + setErrors(newErrors) + return valid + } + + const handleSubmit = async () => { + if (!validateInputs()) { + return + } + try { + updatePost({ + ...editedPost, + media: editedPost.media ? await convertToBase64(editedPost.media) : editedPost.imageUrl + }) + setEditing(false) + setEditedPost(post) + } catch (error) { + console.error(error) + } + } + + const handleReset = () => { + setEditedPost(post) + setErrors({ title: '', description: '', tags: '', media: '' }) + } + + const handleTagInput = (params) => { + return ( + + ) + } + const truncate = (text, wordLimit) => { + const words = text.split(' ') + if (words.length > wordLimit) { + return `${words.slice(0, wordLimit).join(' ')} ...` + } + return text + } + + return ( + + `1px solid ${theme.palette.divider}`, + position: 'relative', + cursor: 'pointer', + height: editing ? 'auto' : '100%' + }} + elevation={1} + > + (editing ? document.getElementById('image-upload').click() : navigate(`/posts/${post.id}`))} + /> + + + + + {editedPost.visibility === 'PUBLIC' ? : } + + + ) : ( + navigate(`/user/${post.authorId}`)} user={post.author} /> + ) + } + title={!editing && post.author.fullName} + subheader={!editing && moment(post.createdAt).format('Do MMM YYYY \\at h:mm a')} + sx={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: 2, + color: 'white', + '& .MuiCardHeader-title': { color: 'white' }, + '& .MuiCardHeader-subheader': { color: 'rgba(255, 255, 255, 0.7)' } + }} + action={ + user?.id === post.authorId && ( + setEditing(!editing)} sx={{ color: 'white' }}> + {editing ? : } + + ) + } + /> + + {editing ? ( + + ) : ( + + {truncate(post.title, 10)} + + )} + {editing ? ( + tag.tag.name)} + onChange={(_, value) => { + setEditedPost((prevPost) => ({ + ...prevPost, + tags: value.length > 8 ? value.slice(-8) : value.map((tag) => ({ tag: { name: tag } })) + })) + setErrors({ ...errors, tags: '' }) + }} + onInputChange={(_, value) => (editedPost.tags.length < 8 ? value : '')} + disableClearable + /> + ) : ( + + {post.tags.map(({ tag }) => `#${tag.name} `)} + + )} + {!editing && post.visibility === 'PRIVATE' && ( + + + + + + )} + {editing ? ( + + ) : ( + + {truncate(post.description, 20)} + + )} + + + + {!editing ? ( + + + {currentReaction ? : } + {post.reactionCount || ''} + + + ) : ( + + + + + + )} + {user?.id === post.authorId && + (editing ? ( + + ) : ( + + ))} + + setReactionAnchorEl(null)} + anchorOrigin={{ vertical: 'top', horizontal: 'left' }} + transformOrigin={{ vertical: 'bottom', horizontal: 'left' }} + disableRestoreFocus + slotProps={{ + paper: { + onMouseEnter: handlePopoverEnter, + onMouseLeave: handlePopoverLeave + } + }} + > + `1px solid ${theme.palette.divider}` }}> + {reactions.map((reaction) => ( + handleReactionSelect(reaction)} + sx={{ + color: reaction === currentReaction ? 'white' : reaction.color, + bgcolor: reaction === currentReaction ? reaction.color : 'transparent' + }} + > + + + ))} + + + + + + ) +} + +export default PostCard diff --git a/client/src/components/PostCardSkeleton.jsx b/client/src/components/PostCardSkeleton.jsx new file mode 100644 index 00000000..86da43ea --- /dev/null +++ b/client/src/components/PostCardSkeleton.jsx @@ -0,0 +1,43 @@ +import { Card, CardHeader, CardContent, Skeleton, CardActions, Stack, CardActionArea, CardMedia, Fade } from '@mui/material' + +const PostCardSkeleton = () => { + return ( + + `1px solid ${theme.palette.divider}`, ':hover': { boxShadow: (theme) => `0px 0px 10px 0px ${theme.palette.primary.main}` }, position: 'relative', height: '100%', minHeight: 350 }} elevation={1}> + + + + + + + + + + + } + title={} + subheader={} + action={ + + + + + + } + /> + + + + + + + + + + + ) +} + +export default PostCardSkeleton diff --git a/client/src/components/PostDetails/CommentSection/index.jsx b/client/src/components/PostDetails/CommentSection/index.jsx deleted file mode 100644 index 6688b251..00000000 --- a/client/src/components/PostDetails/CommentSection/index.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import { useState, useRef, useEffect, useContext } from 'react' -import { Typography, TextField, Button, IconButton, Avatar, Grow, CircularProgress, Grid } from '@mui/material' -import { Delete, Edit } from '@mui/icons-material' -import { useDispatch, useSelector } from 'react-redux' -import { Root, classes } from './styles' -import moment from 'moment' -import { createComment, getComments, deleteComment } from '../../../actions/comments' -import Avaatar from 'avataaars2' -import { SnackbarContext } from '../../../contexts/SnackbarContext' -import { Link } from 'react-router-dom' - -const Comment = ({ data, user, post, mode, handleDelete }) => { - let userId = user?.result.googleId || user?.result?._id - - const { creator, message, createdAt, _id } = data - const canDelete = [creator._id, post.creator._id].includes(userId) - const canEdit = userId === creator._id - return ( - -
- - - {creator.avatar ? ( - - ) : ( - - {creator.name.charAt(0)} - - )} - - - -
- {creator.name} - {moment(createdAt).fromNow()} - - {message} - -
- {userId && ( -
- handleDelete(_id)}> - - - {}}> - - -
- )} -
-
-
- ) -} - -const CommentSection = ({ post, user, mode }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const dispatch = useDispatch() - const [message, setMessage] = useState('') - const commentsRef = useRef() - const userId = user?.result.googleId || user?.result._id - const { isFetchingComments: loading, comments } = useSelector((state) => state.posts) - - useEffect(() => { - const fetchComments = async() => { - dispatch(getComments(post._id, snackBar)); - } - fetchComments(); - }, [post._id]); - - const handleSubmit = (e) => { - e.preventDefault() - const comment = { message: message, post: post._id, creator: userId } - dispatch(createComment(comment, snackBar)) - setMessage('') - } - - const handleDelete = (id) => dispatch(deleteComment(id, snackBar)) - - return loading ? ( - - ) : ( - -
-
- - Comments - -
- - {comments.map((comment) => ( - - ))} - -
-
-
-
- - Write a comment - - - setMessage(e.target.value)} /> - - -
-
- - ) -} - -export default CommentSection diff --git a/client/src/components/PostDetails/CommentSection/styles.js b/client/src/components/PostDetails/CommentSection/styles.js deleted file mode 100644 index d2695450..00000000 --- a/client/src/components/PostDetails/CommentSection/styles.js +++ /dev/null @@ -1,135 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'CommentSection' -export const classes = { - root: `${PREFIX}-root`, - outerContainer: `${PREFIX}-outerContainer`, - innerContainer: `${PREFIX}-innerContainer`, - commentContainer: `${PREFIX}-commentContainer`, - commentBox: `${PREFIX}-commentBox`, - commentItem: `${PREFIX}-commentItem`, - userName: `${PREFIX}-userName`, - comment: `${PREFIX}-comment`, - time: `${PREFIX}-time`, - avatar: `${PREFIX}-avatar`, - avaatar: `${PREFIX}-avaatar`, - textColor: `${PREFIX}-textColor`, - darkTextColor: `${PREFIX}-darkTextColor`, - commentContainerBox: `${PREFIX}-commentContainerBox`, - darkCommentContainerBox: `${PREFIX}-darkCommentContainerBox`, - buttonSubmit: `${PREFIX}-buttonSubmit`, - darkButtonSubmit: `${PREFIX}-darkButtonSubmit`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - margin: '10px 5px', - }, - [`& .${classes.avaatar}`]: { - height: 55, - width: 55, - }, - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - height: 46, - width: 46, - }, - [`& .${classes.outerContainer}`]: { - display: 'flex', - justifyContent: 'space-between', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - }, - }, - [`& .${classes.innerContainer}`]: { - height: 300, - overflowY: 'auto', - marginRight: '0px', - width: '100%', - display: 'block', - [theme.breakpoints.down('sm')]: { - display: 'grid', - justifyItems: 'center', - }, - }, - [`& .${classes.commentContainer}`]: { - width: '100%', - display: 'flex', - }, - [`& .${classes.commentBox}`]: { - width: '100%', - margin: 5, - height: 'fit-content', - borderRadius: 5, - backgroundColor: 'rgba(255, 255, 255, .09)', - display: 'flex', - alignItems: 'center', - paddingLeft: 5, - }, - [`& .${classes.commentItem}`]: { - flexDirection: 'column', - width: '100%', - padding: 10, - }, - [`& .${classes.userName}`]: { - fontWeight: 600, - color: 'white', - }, - [`& .${classes.comment}`]: { - textAlign: 'justify', - fontSize: 'small', - marginLeft: 10, - wordBreak: 'break-word', - whiteSpace: 'pre-wrap', - color: 'white', - }, - [`& .${classes.time}`]: { - textAlign: 'start', - fontSize: 'small', - color: 'rgba(255, 255, 255, .30)', - }, - [`& .${classes.textColor}`]: { - color: 'black', - }, - [`& .${classes.darkTextColor}`]: { - color: 'white', - }, - [`& .${classes.commentContainerBox}`]: { - backgroundColor: 'transparent', - textarea : { - color: 'white' - }, - '& .MuiOutlinedInput-root': { - color: 'white', - }, - '& .MuiFormLabel-root': { - color: 'black', - }, - }, - [`& .${classes.darkCommentContainerBox}`]: { - textarea : { - color: 'white' - }, - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: '#b3b3b3', - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - }, - }, - '& .MuiFormLabel-root': { - color: 'white', - }, - }, - [`& .${classes.buttonSubmit}`]: { - marginTop: 10, - }, - [`& .${classes.darkButtonSubmit}`]: { - marginTop: 10, - '&.Mui-disabled': { - backgroundColor: '#aebfd1' - }, - }, -})) diff --git a/client/src/components/PostDetails/PostDetails.jsx b/client/src/components/PostDetails/PostDetails.jsx deleted file mode 100644 index 1d640c64..00000000 --- a/client/src/components/PostDetails/PostDetails.jsx +++ /dev/null @@ -1,75 +0,0 @@ -import moment from 'moment' -import CommentSection from './CommentSection' -import RecommendedPosts from './RecommendedPosts' -import { useContext, useEffect } from 'react' -import { Paper, Typography, CircularProgress, Divider, Button } from '@mui/material' -import { useDispatch, useSelector } from 'react-redux' -import { useNavigate, useParams } from 'react-router-dom' -import { Root, classes } from './styles' -import { getPost } from '../../actions/posts' -import { SnackbarContext } from '../../contexts/SnackbarContext' -import { ModeContext } from '../../contexts/ModeContext' - -// import { posts, isLoading } from '../../temp' -// const post = posts[0] - -const PostDetails = ({ user }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const { id } = useParams() - const dispatch = useDispatch() - const history = useNavigate() - - useEffect(() => { - async function fetchPosts() { - dispatch(getPost(id, history, snackBar)); - } - fetchPosts(); - }, [id]); - - const { post, isLoading } = useSelector((state) => state.posts) - - const { mode } = useContext(ModeContext); - - return isLoading || !post ? ( - - - - - - ) : ( - - -
-
-
- {post.title} -
- - {post.title} - - - {post.tags.map((tag) => `#${tag} `)} - -
- -
- - {post.message} - - Created by: {post.creator.name} - {moment(post.createdAt).format('Do MMMM YYYY, dddd, h:mm A')} - - -
-
-
- - - -
- ) -} - -export default PostDetails diff --git a/client/src/components/PostDetails/RecommendedPosts.jsx b/client/src/components/PostDetails/RecommendedPosts.jsx deleted file mode 100644 index df6b37ba..00000000 --- a/client/src/components/PostDetails/RecommendedPosts.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Typography, Grid } from '@mui/material' -import { Root, classes } from './styles' -import { PostCard, LoadingCard } from '../User/Cards' -import { useDispatch, useSelector } from 'react-redux' -import { useContext, useEffect } from 'react' -import { getRecommendedPosts } from '../../actions/posts' -import { ModeContext } from '../../contexts/ModeContext' -// import dispatch - -// import { posts, isLoading } from '../../temp' -// const post = posts[0] - -const RecommendedPosts = ({ user, tags, post_id }) => { - const dispatch = useDispatch() - const userId = user?.result.googleId || user?.result._id - - useEffect(() => { - const fetchRecommendedPosts = async() => { - dispatch(getRecommendedPosts(tags.join(','))); - } - fetchRecommendedPosts(); - }, [tags]); - - const { recommendedPosts, isFetchingRecommendedPosts: isLoading } = useSelector((state) => state.posts) - - const posts = recommendedPosts?.filter(({ _id }) => _id !== post_id) - - const { mode } = useContext(ModeContext); - - return ( - - - You might also like: - -
-
- {!isLoading && !posts.length ? ( - - No posts found with these tags - - ) : ( - - {isLoading ? [...Array(10).keys()].map((key) => ) : posts.map((post) => )} - - )} -
- {/* {isLoading ? : setPage(page)} />} */} -
-
- ) -} - -export default RecommendedPosts diff --git a/client/src/components/PostDetails/styles.js b/client/src/components/PostDetails/styles.js deleted file mode 100644 index da5d9a44..00000000 --- a/client/src/components/PostDetails/styles.js +++ /dev/null @@ -1,148 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'PostDetails' -export const classes = { - root: `${PREFIX}-root`, - media: `${PREFIX}-media`, - card: `${PREFIX}-card`, - section: `${PREFIX}-section`, - imageSection: `${PREFIX}-imageSection`, - recommendedPosts: `${PREFIX}-recommendedPosts`, - loadingPaperLight: `${PREFIX}-loadingPaperLight`, - loadingPaperDark: `${PREFIX}-loadingPaperDark`, - commentText: `${PREFIX}-commentText`, - paragraph: `${PREFIX}-paragraph`, - tags: `${PREFIX}-tags`, - title: `${PREFIX}-title`, - darktitle: `${PREFIX}-darktitle`, - privateLabel: `${PREFIX}-privateLabel`, - notFound: `${PREFIX}-notFound`, - recommendedPostGrid: `${PREFIX}-recommendedPostGrid`, - textColor: `${PREFIX}-textColor`, - darkTextColor: `${PREFIX}-darkTextColor`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - margin: '10px 5px', - // padding: '10px', - }, - [`& .${classes.media}`]: { - borderRadius: '5px', - objectFit: 'scale-down', - width: '100%', - maxHeight: '500px', - }, - [`& .${classes.card}`]: { - display: 'flex', - width: '100%', - flexDirection: 'column', - [theme.breakpoints.down('sm')]: { - flexWrap: 'wrap', - }, - }, - [`& .${classes.section}`]: { - borderRadius: '5px', - margin: '10px', - flex: 1, - [theme.breakpoints.down('md')]: { - flexWrap: 'wrap', - }, - }, - [`& .${classes.imageSection}`]: { - [theme.breakpoints.down('sm')]: { - marginLeft: 0, - }, - }, - [`& .${classes.recommendedPosts}`]: { - display: 'flex', - justifyContent: 'center', - minheight: '39vh', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - }, - }, - [`& .${classes.loadingPaperLight}`]: { - display: 'flex', - justifyContent: 'center', - alignItems: 'flex-start', - padding: '20px', - borderRadius: '5px', - minheight: '39vh', - color: 'white', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - flexDirection: 'column', - }, - - [`& .${classes.loadingPaperDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .9)', - }, - [`& .${classes.commentText}`]: { - width: '94%', - display: 'flex', - color: 'white', - overflowWrap: 'break-word', - wordWrap: 'break-word', - hyphens: 'auto', - alignItems: 'center', - [theme.breakpoints.down('sm')]: { - maxWidth: '280px', - }, - [theme.breakpoints.down(385)]: { - maxWidth: '225px', - }, - }, - [`& .${classes.paragraph}`]: { - wordBreak: 'break-word', - textAlign: 'justify', - color: 'white', - }, - [`& .${classes.tags}`]: { - textAlign: 'center', - }, - [`& .${classes.title}`]: { - textAlign: 'center', - color: 'black', - wordBreak: 'break-word', - [theme.breakpoints.down('sm')]: { - fontSize: '30px', - fontWeight: 'bolder', - marginBottom: '20px', - }, - }, - [`& .${classes.darktitle}`]: { - color: 'white' - }, - [`& .${classes.privateLabel}`]: { - backgroundColor: '#00b5ff', - align: 'center', - }, - [`& .${classes.notFound}`]: { - justifyContent: 'space-around', - flexDirection: 'row', - display: 'flex', - alignItems: 'center', - color: 'white', - width: '100%', - }, - [`& .${classes.recommendedPostGrid}`]: { - justifyContent: 'space-around', - marginTop: 5, - marginLeft: 0, - flexDirection: 'row', - width: '100%', - }, - [`& .${classes.textColor}`]: { - color: 'black' - }, - [`& .${classes.darkTextColor}`]: { - color: 'white' - }, -})) - -export default Root diff --git a/client/src/components/PostSkeleton.jsx b/client/src/components/PostSkeleton.jsx new file mode 100644 index 00000000..7080629e --- /dev/null +++ b/client/src/components/PostSkeleton.jsx @@ -0,0 +1,59 @@ +import { Skeleton, Box, Card, CardActions, CardContent, CardHeader, Divider, Stack } from '@mui/material' + +const AuthorInfoSkeleton = () => ( + + + + + + + + + + +) + +const PostActionsSkeleton = () => ( + + + {[1, 2].map((item) => ( + + + + + ))} + + + + + + +) + +const PostSkeleton = () => { + return ( + + + }> + } /> + + + + {[1, 2, 3].map((item) => ( + + ))} + + {[1, 2, 3].map((item) => ( + + ))} + + + + + + + + ) +} + +export default PostSkeleton diff --git a/client/src/components/Posts/Post/index.jsx b/client/src/components/Posts/Post/index.jsx deleted file mode 100644 index 24bd85e9..00000000 --- a/client/src/components/Posts/Post/index.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import ThumbUpAltIcon from '@mui/icons-material/ThumbUpAlt' -import ThumbUpAltOutlined from '@mui/icons-material/ThumbUpAltOutlined' -import DeleteIcon from '@mui/icons-material/Delete' -import MoreHorizIcon from '@mui/icons-material/MoreHoriz' -import moment from 'moment' -import { useContext, useState } from 'react' -import { CardActions, CardContent, CardMedia, Button, Typography, ButtonBase, Card, Avatar, Tooltip } from '@mui/material' -import { Root, classes } from './styles' -import { useDispatch } from 'react-redux' -import { deletePost, updatePost } from '../../../actions/posts' -import { Link, useNavigate } from 'react-router-dom' -import { SnackbarContext } from '../../../contexts/SnackbarContext' -import DeleteDialogBox from '../../DialogBox/DeleteDialogBox' -import Avaatar from 'avataaars2' -import { ModeContext } from '../../../contexts/ModeContext' - -const Post = ({ post, setCurrentId, user }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const dispatch = useDispatch() - const history = useNavigate() - const [likes, setLikes] = useState(post?.likes) - const userId = user && (user.result.googleId || user.result._id) - const hasLikedPost = likes.find((like) => like === userId) - const [deleteing, setDeleteing] = useState(false) - - const handleLike = () => { - const usersLiked = hasLikedPost ? likes.filter((id) => id !== userId) : [...likes, userId] - setLikes(usersLiked) - dispatch(updatePost(post._id, { ...post, likes: usersLiked })) - } - - const handleDelete = () => { - dispatch(deletePost(post._id, snackBar, setDeleteing)) - } - const Likes = () => { - if (likes.length > 0) - return hasLikedPost ? ( - - -   {likes.length > 2 ? `You and ${likes.length - 1} others` : `${likes.length} Like${likes.length > 1 ? 's' : ''}`} - - ) : ( - - -   {`${likes.length} Like${likes.length > 1 ? 's' : ''}`} - - ) - return ( - - -   Like - - ) - } - const isLongMessage = post.message.length > 100 - const openPost = () => history(`/posts/${post._id}`) - - const { mode } = useContext(ModeContext) - - return ( - - - - - -
-
- - {post.tags.map((tag) => `#${tag} `)} - -
- - - {post.title} - - {post.private && ( -
- -
- )} - - - {`${post.message.slice(0, 100)} ${isLongMessage ? '...' : ''}`} - - -
-
-
-
-
- - {post.creator.avatar ? ( - - ) : ( - - {post.creator.name?.charAt(0)} - - )} - -
-
- - {post.creator.name} - - - - {moment(post.createdAt).fromNow()} - - -
-
-
- {userId === post.creator._id && ( -
- -
- )} - - - {userId === post.creator._id && ( - - )} - -
-
- ) -} - -export default Post diff --git a/client/src/components/Posts/Post/styles.js b/client/src/components/Posts/Post/styles.js deleted file mode 100644 index 77fa59bb..00000000 --- a/client/src/components/Posts/Post/styles.js +++ /dev/null @@ -1,122 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Post' -export const classes = { - root: `${PREFIX}-root`, - media: `${PREFIX}-media`, - border: `${PREFIX}-border`, - fullHeightCard: `${PREFIX}-fullHeightCard`, - cardLight: `${PREFIX}-cardLight`, - cardDark: `${PREFIX}-cardDark`, - overlay: `${PREFIX}-overlay`, - overlay2: `${PREFIX}-overlay2`, - grid: `${PREFIX}-grid`, - tags: `${PREFIX}-tags`, - details: `${PREFIX}-details`, - title: `${PREFIX}-title`, - cardAction: `${PREFIX}-cardAction`, - cardActions: `${PREFIX}-cardActions`, - privateLabel: `${PREFIX}-privateLabel`, - avatar: `${PREFIX}-avatar`, - avaatar: `${PREFIX}-avaatar`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - height: '100%', - }, - [`& .${classes.avaatar}`]: { - height: 50, - width: 50, - }, - [`& .${classes.avatar}`]: { - height: 46, - width: 46, - }, - [`& .${classes.media}`]: { - paddingTop: '56.25%', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - backgroundBlendMode: 'darken', - transition: 'transform .2s', - }, - [`& .${classes.cardLight}`]: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - borderRadius: 5, - height: '100%', - position: 'relative', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - transition: 'transform .2s', - '&:hover': { - transform: 'scale(1.05)', - }, - }, - [`& .${classes.overlay}`]: { - position: 'absolute', - top: 20, - left: 20, - }, - [`& .${classes.overlay2}`]: { - position: 'absolute', - top: 20, - right: 20, - }, - [`& .${classes.grid}`]: { - display: 'flex', - }, - [`& .${classes.tags}`]: { - display: 'flex', - justifyContent: 'space-between', - margin: 20, - }, - [`& .${classes.details}`]: { - display: 'flex', - flexDirection: 'column', - }, - [`& .${classes.title}`]: { - padding: '0 16px', - }, - [`& .${classes.cardActions}`]: { - padding: '0 16px 8px 16px', - display: 'flex', - justifyContent: 'space-between', - '& .MuiButtonBase-root': { - '& .MuiTypography-root': { - display: 'flex', - align: 'center', - }, - color:'#000000', - }, - }, - [`& .${classes.cardAction}`]: { - display: 'block', - textAlign: 'initial', - - }, - [`& .${classes.privateLabel}`]: { - backgroundColor: '#00b5ff', - align: 'center', - - }, - [`& .${classes.cardDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .90)', //"transparent" //"rgba(69, 114, 200)" - [`& .${classes.tags}`]: { - '& .MuiTypography-root': { - color: '#b3b3b3' - } - }, - [`& .${classes.title}`]: { - color: 'white', - }, - [`& .${classes.cardActions}`]: { - - '& .MuiButtonBase-root': { - color: '#ffffff', - }, - } - }, -})) - -export default Root diff --git a/client/src/components/Posts/index.jsx b/client/src/components/Posts/index.jsx deleted file mode 100644 index a62e8401..00000000 --- a/client/src/components/Posts/index.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import Post from './Post' -import { Grid, CircularProgress, Grow } from '@mui/material' -import { useSelector } from 'react-redux' -import { Root, classes } from './styles' - -// import { posts, isLoading } from "../../temp" - -const Posts = ({ setCurrentId, user }) => { - const { posts, isLoading } = useSelector((state) => state.posts) // [] -> { isLoading, posts: [] } - if (!posts.length && !isLoading) return 'No Posts' - return ( - - {isLoading ? ( - - ) : ( - - {posts.map((post) => ( - - - - - - ))} - - )} - - ) -} - -export default Posts diff --git a/client/src/components/Posts/styles.js b/client/src/components/Posts/styles.js deleted file mode 100644 index 983a8afe..00000000 --- a/client/src/components/Posts/styles.js +++ /dev/null @@ -1,30 +0,0 @@ -import { styled } from "@mui/material/styles" - -const PREFIX = "Posts" -export const classes = { - root: `${PREFIX}-root`, - mainContainer: `${PREFIX}-mainContainer`, - smMargin: `${PREFIX}-smMargin`, - actionDiv: `${PREFIX}-actionDiv`, -} - -export const Root = styled("div")(({ theme }) => ({ - [`&.${classes.root}`]: { - display: "flex", - justifyContent: "center", - minHeight: 150, - alignItems: "center", - }, - [`& .${classes.mainContainer}`]: { - display: "flex", - alignItems: "center", - }, - [`& .${classes.smMargin}`]: { - margin: theme.spacing(1), - }, - [`& .${classes.actionDiv}`]: { - textAlign: "center", - }, -})) - -export default Root diff --git a/client/src/components/ScrollToTop.jsx b/client/src/components/ScrollToTop.jsx new file mode 100644 index 00000000..eb969336 --- /dev/null +++ b/client/src/components/ScrollToTop.jsx @@ -0,0 +1,22 @@ +import { Box, Fab, Fade, Tooltip, useScrollTrigger } from '@mui/material' +import { KeyboardArrowUp } from '@mui/icons-material' + +// Floating Action Button to scroll to top +const ScrollToTop = () => { + const handleClick = () => window.scrollTo({ top: 0, behavior: 'smooth' }) + const trigger = useScrollTrigger() + + return ( + + + + + + + + + + ) +} + +export default ScrollToTop diff --git a/client/src/components/ScrollToTop/index.jsx b/client/src/components/ScrollToTop/index.jsx deleted file mode 100644 index 5a17fb0f..00000000 --- a/client/src/components/ScrollToTop/index.jsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useState, useEffect } from 'react' -import { Box, Fab, Fade } from '@mui/material' -import { KeyboardArrowUp } from '@mui/icons-material' - -// Floating Action Button to scroll to top -const ScrollToTop = () => { - const [isVisible, setIsVisible] = useState(false) - const handleClick = () => window.scrollTo({ top: 0, behavior: 'smooth' }) - - useEffect(() => { - // Button is displayed after scrolling for 200 pixels - const toggleVisibility = () => setIsVisible(window.pageYOffset > 200 ? true : false) - window.addEventListener('scroll', toggleVisibility) - return () => window.removeEventListener('scroll', toggleVisibility) - }, []) - - return ( - - - - - - - - ) -} - -export default ScrollToTop diff --git a/client/src/components/Search/Search.jsx b/client/src/components/Search/Search.jsx deleted file mode 100644 index 409b6a0d..00000000 --- a/client/src/components/Search/Search.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import { useContext, useState,useEffect} from 'react' -import { useSelector } from 'react-redux' -// import ChipInput from '../ChipInput/ChipInput' -import { MuiChipsInput } from 'mui-chips-input' -import { Root, classes } from './styles' -import { useNavigate } from 'react-router-dom' -import { useDispatch } from 'react-redux' - -import { getPostsBySearch } from '../../actions/posts' -import { AppBar, TextField, Button } from '@mui/material' -import { ModeContext } from '../../contexts/ModeContext' -import Autocomplete from '@mui/material/Autocomplete'; - -import * as api from '../../api/index' -const Search = ({ tags, setTags }) => { - - const dispatch = useDispatch() - const { posts, isLoading } = useSelector((state) => state.posts) - - - const [selecttags,Setselecttags]=useState(''); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await api.fetchTags(); - - Setselecttags(response.data.data); - } catch (error) { - console.error('Error fetching tags:', error); - // Handle the error as needed, e.g., show a message to the user - } - }; - - fetchData(); - - - }, []); - - - const history = useNavigate() - const [search, setSearch] = useState('') - - - const searchPost = () => { - //dispatch -> fetch search post - if (search.trim() || tags) { - let tagList = tags.map((tag) => tag.toLowerCase()); - dispatch(getPostsBySearch({ search, tags: tagList.join(',') })) - history(`/posts/search?searchQuery=${search || 'none'}&tags=${tagList.join(',')}`) - } else history('/') - } - - const handleKeyPress = (e) => { - if (e.key === 'Enter') searchPost() - } - - // const handleAdd = (tag) => setTags([...tags, tag]) - // const handleDelete = (tagToDelete) => setTags(tags.filter((tag) => tag !== tagToDelete)) - const handleChange = (newTags) => { - alert("hello"); - console.log(newTags); - setTags(newTags) - } - - const { mode } = useContext(ModeContext) - - return ( - - - setSearch(e.target.value)} /> - {/* */} - {/* */} - - { - setTags(newTags); - - }} - renderInput={(params) => ( - - )} - /> - - - - - ) -} - -export default Search diff --git a/client/src/components/Search/styles.js b/client/src/components/Search/styles.js deleted file mode 100644 index a237e555..00000000 --- a/client/src/components/Search/styles.js +++ /dev/null @@ -1,54 +0,0 @@ -import { styled } from "@mui/material/styles" - -const PREFIX = "Search" -export const classes = { - root: `${PREFIX}-root`, - searchBarLight: `${PREFIX}-searchBarLight`, - searchBarDark: `${PREFIX}-searchBarDark`, - searchButton: `${PREFIX}-searchButton`, - chip: `${PREFIX}-chip`, -} - -export const Root = styled("div")(({ theme }) => ({ - [`&.${classes.root}`]: { - "& .MuiTextField-root": { - margin: theme.spacing(0.5, 0), - }, - "& .MuiFormLabel-root": { - color: "white", - }, - "& .MuiChip-filled": { - background: "#ffffff70", - }, - marginTop: "15px", - }, - [`& .${classes.searchBarLight}`]: { - marginBottom: "1rem", - display: "flex", - padding: theme.spacing(2), - borderRadius: "5px", - backgroundColor: "rgba(255, 255, 255, .09)", - backdropFilter: "blur(10px)", - }, - [`& .${classes.searchBarDark}`]: { - backgroundColor: "rgba(5, 5, 5, .90)", - '& .MuiOutlinedInput-root': { - '& fieldset': { - borderColor: '#b3b3b3', - }, - '&.Mui-focused fieldset': { - borderColor: theme.palette.primary.main, - }, - }, - }, - - [`& .${classes.searchButton}`]: { - marginTop: 10, - }, - - [`& .${classes.chip}`]: { - paddingBottom: "10px", - }, -})) - -export default Root diff --git a/client/src/components/Searchbar.jsx b/client/src/components/Searchbar.jsx new file mode 100644 index 00000000..70ac6fc2 --- /dev/null +++ b/client/src/components/Searchbar.jsx @@ -0,0 +1,91 @@ +import { Search as SearchIcon } from '@mui/icons-material' +import { alpha, Box, InputBase, styled } from '@mui/material' +import { useState } from 'react' +import { useLocation } from 'react-router-dom' +import { useStore } from '@/store' + +const Search = styled(Box)(({ theme }) => ({ + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: alpha(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: alpha(theme.palette.common.white, 0.25) + }, + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('xs')]: { + width: 'auto' + } +})) + +const SearchIconWrapper = styled(Box)(({ theme }) => ({ + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center' +})) +const StyledInputBase = styled(InputBase)(({ theme }) => ({ + color: 'inherit', + width: '100%', + + '& .MuiInputBase-input': { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)})`, + transition: theme.transitions.create('width'), + [theme.breakpoints.up('xs')]: { + width: '20ch', + '&:focus': { + width: '33ch' + } + } + } +})) +const Searchbar = () => { + const [search, setSearch] = useState('') + const { pathname } = useLocation() + const { openSnackbar } = useStore() + const handleSubmit = (event) => { + event.preventDefault() + if (!search) { + return + } + const securityLevels = ['info', 'warning', 'error', 'success'] + + const lowerCaseSearch = search.toLowerCase() + const foundLevel = securityLevels.find((level) => lowerCaseSearch.includes(level)) + + if (foundLevel) { + openSnackbar({ + severity: foundLevel || 'success', + message: `Alert! You searched for a term containing "${foundLevel}".` + }) + } else { + openSnackbar({ + severity: 'success', + message: `Hurrray! 🎊🎊, You searched for "${search}"` + }) + } + setSearch('') + } + + const handleChange = (e) => setSearch(e.target.value) + + if (pathname !== '/posts') { + return null + } + + return ( + + + + + + + ) +} + +export default Searchbar diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx new file mode 100644 index 00000000..9da3e564 --- /dev/null +++ b/client/src/components/Sidebar.jsx @@ -0,0 +1,102 @@ +import { brand as logo } from '@/assets' +import { Avatar, IconButton, ListItemText, ListItemIcon, ListItemButton, ListItem, List, Divider, Button, SwipeableDrawer, Box, ListItemAvatar } from '@mui/material' +import { Close, Dashboard, GitHub, Logout, Settings } from '@mui/icons-material' + +import ThemeSwitch from './ThemeSwitch' +import { Link } from 'react-router-dom' +import { useAuth, useUser } from '@clerk/clerk-react' + +const SideBar = ({ open, setOpen }) => { + const closeDrawer = () => setOpen(false) + const { user } = useUser() + const { signOut } = useAuth() + + return ( + + + + + + + + + {!user && ( + + + + + + + + + + )} + {user && ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )} + {!user && ( + + + + + + + + + )} + + + + + + + + + + + + + ) +} + +export default SideBar diff --git a/client/src/components/SnackBar/index.jsx b/client/src/components/SnackBar/index.jsx deleted file mode 100644 index 02a06eba..00000000 --- a/client/src/components/SnackBar/index.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import { forwardRef, useCallback } from 'react' -import { Snackbar, Alert } from '@mui/material' - -const MuiAlert = forwardRef((props, ref) => ) - -const SnackBar = ({ open, setOpen, alertSeverity, snackBarMessage }) => { - const handleClose = useCallback(() => setOpen(false)) - - return ( - - - {snackBarMessage} - - - ) -} - -export default SnackBar diff --git a/client/src/components/Snackbar.jsx b/client/src/components/Snackbar.jsx new file mode 100644 index 00000000..4220f040 --- /dev/null +++ b/client/src/components/Snackbar.jsx @@ -0,0 +1,26 @@ +import { Alert, Snackbar as MUISnackbar, Slide } from '@mui/material' +import { useStore } from '@/store' + +const Snackbar = () => { + const { snackbar, closeSnackbar } = useStore() + const { message, open, severity } = snackbar + + return ( + + + {message} + + + ) +} + +export default Snackbar \ No newline at end of file diff --git a/client/src/components/SuspenseFallback.jsx b/client/src/components/SuspenseFallback.jsx new file mode 100644 index 00000000..f9bc6ea3 --- /dev/null +++ b/client/src/components/SuspenseFallback.jsx @@ -0,0 +1,23 @@ +import { CircularProgress, Stack, Typography } from '@mui/material' + +const SuspenseFallback = () => { + return ( + + + + Loading... + + + ) +} + +export default SuspenseFallback diff --git a/client/src/components/ThemeSwitch.jsx b/client/src/components/ThemeSwitch.jsx new file mode 100644 index 00000000..24cefc23 --- /dev/null +++ b/client/src/components/ThemeSwitch.jsx @@ -0,0 +1,79 @@ +import { Computer, DarkMode, LightMode, SettingsSystemDaydream } from '@mui/icons-material' +import { Button, ButtonGroup, IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material' +import { useState } from 'react' +import { useTheme } from '@/hooks' + +const ThemeMenu = ({ anchorEl, handleClose, handleClick, theme }) => { + return ( + + handleClick('light')} selected={theme === 'light'}> + Light + + + + + handleClick('dark')} selected={theme === 'dark'}> + Dark + + + + + handleClick('system')} selected={theme === 'system'}> + System + + + + + + ) +} +const ThemeSwitch = () => { + const [anchorEl, setAnchorEl] = useState(null) + const { theme, setTheme } = useTheme() + + const handleOpen = (event) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const handleClick = (newTheme) => { + setTheme(newTheme) + handleClose() + } + + return ( + <> + + {theme === 'light' ? : theme === 'dark' ? : } + + + + + + + + + ) +} +export default ThemeSwitch diff --git a/client/src/components/User/Cards/index.jsx b/client/src/components/User/Cards/index.jsx deleted file mode 100644 index b8df1b9b..00000000 --- a/client/src/components/User/Cards/index.jsx +++ /dev/null @@ -1,132 +0,0 @@ -import { CardActions, CardHeader, Card, CardContent, CardMedia, Grid, Grow, Typography, Button, Skeleton, ButtonBase } from '@mui/material' -import { ThumbUpAlt, Lock, ThumbUpAltOutlined } from '@mui/icons-material' -import { Link, useNavigate } from 'react-router-dom' -import { Root, classes, Comment, Media } from './styles' -import moment from 'moment' -import { useContext } from 'react' -import { ModeContext } from '../../../contexts/ModeContext' - -export const LoadingCard = () => { - return ( - - - - } subheader={} /> - - - - - - - ) -} - -export const CommentLoadingCard = () => { - return ( - - - - -
- - - -
-
-
-
- ) -} - -export const CommentCard = ({ message, createdAt, post: { _id: postId, thumbnail: media, title } }) => { - const { mode, modeToggle } = useContext(ModeContext) - return ( - - - - - - -
- {title} - {moment(createdAt).fromNow()} - - {message} - -
-
-
-
- ) -} - -export const PostCard = ({ post, userId }) => { - const { mode, modeToggle } = useContext(ModeContext) - const history = useNavigate() - const { title, message, name, tags, thumbnail, likes, createdAt, _id } = post - const hasLikedPost = likes.find((like) => like === userId) - const Likes = () => { - const textColor = mode === 'dark' ? 'white' : '#000000'; - if (likes.length > 0) - return hasLikedPost ? ( - - -   {likes.length > 2 ? `You and ${likes.length - 1} others` : `${likes.length} Like${likes.length > 1 ? 's' : ''}`} - - ) : ( - - -   {`${likes.length} Like${likes.length > 1 ? 's' : ''}`} - - ) - return ( - - -   Like - - ) - } - return ( - - - history(`/posts/${_id}`)} component="span"> - -
-
- {name} - {moment(createdAt).fromNow()} -
- - - {tags - .map((tag) => `#${tag} `) - .join(' ') - .slice(0, 50)} - - -
- - {title.slice(0, 25)} - - - - - {`${message.slice(0, 100)} ${message.length > 100 ? '...' : ''}`} - - -
- - - -
-
-
-
- ) -} - -export default PostCard diff --git a/client/src/components/User/Cards/styles.js b/client/src/components/User/Cards/styles.js deleted file mode 100644 index 897b5723..00000000 --- a/client/src/components/User/Cards/styles.js +++ /dev/null @@ -1,134 +0,0 @@ -import { styled } from '@mui/material/styles' -import { CardMedia, Grow as MUIGrow } from '@mui/material' -const PREFIX = 'Cards' -export const classes = { - root: `${PREFIX}-root`, - loadingCard: `${PREFIX}-loadingCard`, - loadingCardHeader: `${PREFIX}-loadingCardHeader`, - postCard: `${PREFIX}-postCard`, - buttonBase: `${PREFIX}-buttonBase`, - cardMedia: `${PREFIX}-cardMedia`, - cardContent: `${PREFIX}-cardContent`, - overlay: `${PREFIX}-overlay`, - cardActions: `${PREFIX}-cardActions`, - commentContainer: `${PREFIX}-commentContainer`, - commentBox: `${PREFIX}-commentBox`, - commentItem: `${PREFIX}-commentItem`, - userName: `${PREFIX}-userName`, - comment: `${PREFIX}-comment`, - time: `${PREFIX}-time`, - darkModeText: `${PREFIX}-darkModeText`, -} - -export const Root = styled('div')(({ mode }) => ({ - [`& .${classes.loadingCard}`]: { - maxWidth: 345, - margin: 16, - width: 300, - backgroundColor: 'transparent', - }, - [`& .${classes.loadingCardHeader}`]: { - marginBottom: 6, - height: 10, - width: '80%', - }, - [`& .${classes.postCard}`]: { - width: 300, - maxWidth: 300, - margin: 16, - backgroundColor: 'transparent', - backdropFilter: 'blur(10px)', - border: mode === 'dark' ? '2px solid gray' : '', - }, - [`& .${classes.buttonBase}`]: { - width: '100%', - padding: '0', - display: 'inline-flex', - justifyContent: 'space-between', - flexDirection: 'column', - }, - [`& .${classes.cardMedia}`]: { - height: 200, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - backgroundBlendMode: 'darken', - filter: 'brightness(.8)', - }, - [`& .${classes.cardContent}`]: { - display: 'flex', - flexDirection: 'column', - width: '100%', - height: 270, - }, - [`& .${classes.overlay}`]: { - position: 'absolute', - top: 20, - left: 20, - '& .MuiTypography-root': { - color: 'white', - }, - }, - [`& .${classes.cardActions}`]: { - padding: '0 16px 8px 16px', - display: 'flex', - justifyContent: 'space-between', - width: '100%', - position: 'absolute', - bottom: 10, - left: 10, - }, - [`& .${classes.darkModeText}`]: { - color: 'white', - }, - -})) - -export const Media = styled(CardMedia)(({theme}) => ({ - borderRadius: 5, - height: 90, - width: 130, - [theme.breakpoints.down('sm')]: { - height: '67px', - width: '100px' - } -})) - -export const Comment = styled(MUIGrow)(() => ({ - width: '100%', - [`& .${classes.commentContainer}`]: { - display: 'flex', - textDecoration: 'none', - }, - [`& .${classes.commentBox}`]: { - width: '100%', - margin: 5, - height: 'fit-content', - borderRadius: 5, - backgroundColor: 'rgba(255, 255, 255, .09)', - display: 'flex', - alignItems: 'center', - padding: 5, - }, - [`& .${classes.commentItem}`]: { - flexDirection: 'column', - width: '100%', - padding: 10, - }, - [`& .${classes.userName}`]: { - fontWeight: 600, - color: 'black', - }, - [`& .${classes.comment}`]: { - fontSize: 'small', - wordBreak: 'break-word', - whiteSpace: 'pre-wrap', - color: 'white', - }, - [`& .${classes.time}`]: { - textAlign: 'start', - fontSize: 'small', - color: 'rgba(255, 255, 255, .30)', - }, - [`& .${classes.darkModeText}`]: { - color: 'white', - }, -})) diff --git a/client/src/components/User/Details/index.jsx b/client/src/components/User/Details/index.jsx deleted file mode 100644 index 83f3ccb2..00000000 --- a/client/src/components/User/Details/index.jsx +++ /dev/null @@ -1,377 +0,0 @@ -import { useRef, useContext, useEffect, useState } from 'react' -import { Root, classes } from './styles' -import { Paper, Typography, Divider, Avatar, LinearProgress, Box, Chip, Tabs, Tab, Button, Tooltip } from '@mui/material' -import { PublishedWithChanges } from '@mui/icons-material' -import { useSelector, useDispatch } from 'react-redux' -import Avaatar from 'avataaars2' -import { getUserDetails, getPostsBySearch, getUserComments, getUserPostsByType } from '../../../actions/posts' -import TabPage from '../TabPage' -import { useNavigate, Link, useParams } from 'react-router-dom' - -import { SwipeableViews } from 'react-swipeable-views-v18' -import { useSwipe } from '../../../hooks' -import { useTheme } from '@mui/material/styles' -import { SnackbarContext } from '../../../contexts/SnackbarContext' -import { ModeContext } from '../../../contexts/ModeContext' - -const LinearProgressWithLabel = (props) => ( - - - - - - {`${Math.round(props.value)}%`} - - -) -const TabPanel = ({ children, value, index, ...other }) => ( - -) - -const CREATED = 'created' -const LIKED = 'liked' -const PRIVATE = 'private' - -const UserDetails = ({ user }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const { mode, modeToggle } = useContext(ModeContext) - const theme = useTheme() - const history = useNavigate() - const dispatch = useDispatch() - - const [idx, setIdx] = useState(0) - const [progress, setProgress] = useState(0) - const [likedPage, setLikedPage] = useState(1) - const [createdPage, setCreatedPage] = useState(1) - const [privatePage, setPrivatePage] = useState(1) - const [commentsPage, setCommentsPage] = useState(1) - - const { data, isLoading } = useSelector((state) => state.posts) - const { createdPosts, createdNumberOfPages, isFetchingCreatedPosts } = useSelector((state) => state.posts) - const { likedPosts, likedNumberOfPages, isFetchingLikedPosts } = useSelector((state) => state.posts) - const { privatePosts, privateNumberOfPages, isFetchingPrivatePosts } = useSelector((state) => state.posts) - const { userComments: comments, commentsNumberOfPages, isFetchingComments } = useSelector((state) => state.posts) - - const userId = user.result._id || user.result.googleId - - useEffect(() => { - const fetchUserDetails = async () => dispatch(getUserDetails(userId, snackBar)) - fetchUserDetails() - }, [userId]) - useEffect(() => { - const fetchUserPostsByType = async () => dispatch(getUserPostsByType(userId, createdPage, CREATED)) - fetchUserPostsByType() - }, [createdPage]) - useEffect(() => { - const fetchUserPostsByType = async () => dispatch(getUserPostsByType(userId, likedPage, LIKED)) - fetchUserPostsByType() - }, [likedPage]) - useEffect(() => { - const fetchUserPostsByType = async () => dispatch(getUserPostsByType(userId, privatePage, PRIVATE)) - fetchUserPostsByType() - }, [privatePage]) - useEffect(() => { - const fetchUserComments = async () => dispatch(getUserComments(userId, commentsPage)) - fetchUserComments() - }, [commentsPage]) - - useEffect(() => { - const timer = setInterval(() => { - setProgress((prevProgress) => (prevProgress >= 90 ? (isLoading ? 90 : 100) : prevProgress + 10)) - }, 300) - return () => clearInterval(timer) - }, [isLoading]) - - const openPostsWithTag = (tag) => { - dispatch(getPostsBySearch({ tags: tag })) - history(`/posts/search?searchQuery=none&tags=${tag}`) - } - - const { postsCreated, postsLiked, privatePosts: numberOfPrivatePosts, totalLikesRecieved, longestPostWords, top5Tags, longestPostId } = data - const labels = { - Email: user.result.email, - 'Posts Created f': postsCreated, - 'Posts Liked': postsLiked, - 'Private Posts': numberOfPrivatePosts, - 'Liked Recived': totalLikesRecieved, - } - const createdProps = { - page: createdPage, - setPage: setCreatedPage, - posts: createdPosts, - numberOfPages: createdNumberOfPages, - isLoading: isFetchingCreatedPosts, - userId: userId, - notDoneText: 'No Posts Created', - } - const likedProps = { - page: likedPage, - setPage: setLikedPage, - posts: likedPosts, - numberOfPages: likedNumberOfPages, - isLoading: isFetchingLikedPosts, - userId: userId, - notDoneText: 'No Posts Liked', - } - const privateProps = { - page: privatePage, - setPage: setPrivatePage, - posts: privatePosts, - numberOfPages: privateNumberOfPages, - isLoading: isFetchingPrivatePosts, - user: userId, - notDoneText: 'No Posts Private', - } - const commentProps = { - page: commentsPage, - setPage: setCommentsPage, - comments: comments, - numberOfPages: commentsNumberOfPages, - isLoading: isFetchingComments, - user: userId, - notDoneText: 'No Comments posted', - } - - // change views - const swipeableViewsRef = useRef(null) - useSwipe(swipeableViewsRef, idx) - - return ( - -
- - {user.result.avatar ? ( - - ) : ( - - - {user.result.name.charAt(0)} - - - )} - - - - {progress < 100 || isLoading ? ( - - Loading User Details ... - - - ) : ( -
- {Object.entries(labels).map(([label, newdata]) => ( - - - {label}: - {newdata} - - - - ))} - - - Longest Post Written: - - {`${longestPostWords} Words`} - - - - -
- - Top 5 Tags: - - {top5Tags?.length ? top5Tags.map((tag) => openPostsWithTag(tag)} className={classes.chips} />) : } -
-
- )} - - πŸŽ‰New UserπŸŽ‰ - -
-
- - - setIdx(newValue)} aria-label="basic tabs" variant="scrollable"> - - - - - - - - - - - - - - - - - - - - - -
- ) -} - -export const PublicProfile = () => { - const { id: userId } = useParams() - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const theme = useTheme() - const history = useNavigate() - const dispatch = useDispatch() - const [idx, setIdx] = useState(0) - const [progress, setProgress] = useState(0) - const [likedPage, setLikedPage] = useState(1) - const [createdPage, setCreatedPage] = useState(1) - const { data, isLoading } = useSelector((state) => state.posts) - const { createdPosts, createdNumberOfPages, isFetchingCreatedPosts } = useSelector((state) => state.posts) - const { likedPosts, likedNumberOfPages, isFetchingLikedPosts } = useSelector((state) => state.posts) - - useEffect(() => { - const fetchUserDetails = async () => dispatch(getUserDetails(userId, snackBar)) - fetchUserDetails() - }, [userId]) - useEffect(() => { - const fetchUserPostsByType = async () => dispatch(getUserPostsByType(userId, createdPage, CREATED)) - fetchUserPostsByType() - }, [createdPage]) - useEffect(() => { - const fetchUserPostsByType = async () => dispatch(getUserPostsByType(userId, likedPage, LIKED)) - fetchUserPostsByType() - }, [likedPage]) - - useEffect(() => { - const timer = setInterval(() => { - setProgress((prevProgress) => (prevProgress >= 90 ? (isLoading ? 90 : 100) : prevProgress + 10)) - }, 300) - return () => clearInterval(timer) - }, [isLoading]) - - // useEffect(() => { - // if (userId && [user?.result._id || user?.result.googleId].includes(userId)) { - // history('/user') - // } - // }, [userId, user]) - - const openPostsWithTag = (tag) => { - dispatch(getPostsBySearch({ tags: tag })) - history(`/posts/search?searchQuery=none&tags=${tag}`) - } - - const { email, name, postsCreated, postsLiked, totalLikesRecieved, longestPostWords, top5Tags, longestPostId } = data - - const labels = { - Name: name, - Email: email, - 'Posts Created': postsCreated, - 'Posts Liked': postsLiked, - 'Liked Recived': totalLikesRecieved, - } - const createdProps = { - page: createdPage, - setPage: setCreatedPage, - posts: createdPosts, - numberOfPages: createdNumberOfPages, - isLoading: isFetchingCreatedPosts, - userId: userId, - notDoneText: 'No Posts Created', - } - const likedProps = { - page: likedPage, - setPage: setLikedPage, - posts: likedPosts, - numberOfPages: likedNumberOfPages, - isLoading: isFetchingLikedPosts, - userId: userId, - notDoneText: 'No Posts Liked', - } - - // change views - const swipeableViewsRef = useRef(null) - useSwipe(swipeableViewsRef, idx) - const { mode, modeToggle } = useContext(ModeContext) - - return ( - -
- - {progress < 100 || isLoading ? ( - - ) : data.avatar ? ( - - ) : ( - - - {data.name.charAt(0)} - - - )} - - - {progress < 100 || isLoading ? ( - - Loading User Details ... - - - ) : ( -
- {Object.entries(labels).map(([label, labelData]) => ( - - - {label}: - {labelData} - - - - ))} - - - Longest Post Written: - - {`${longestPostWords} Words`} - - - - -
- - Top 5 Tags: - - {top5Tags.length ? top5Tags.map((tag) => openPostsWithTag(tag)} className={classes.chips} />) : } -
-
- )} - - πŸŽ‰New UserπŸŽ‰ - -
-
- - - setIdx(newValue)} aria-label="basic tabs"> - - - - - - - - - - - - - -
- ) -} -export default UserDetails diff --git a/client/src/components/User/Details/styles.js b/client/src/components/User/Details/styles.js deleted file mode 100644 index 7ba20020..00000000 --- a/client/src/components/User/Details/styles.js +++ /dev/null @@ -1,129 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'UserDetails' -export const classes = { - root: `${PREFIX}-root`, - userContainer: `${PREFIX}-userContainer`, - userIcon: `${PREFIX}-userIcon`, - avatar: `${PREFIX}-avatar`, - userDetails: `${PREFIX}-userDetails`, - loadingLine: `${PREFIX}-loadingLine`, - tagsContainer: `${PREFIX}-tagsContainer`, - loadingPaper: `${PREFIX}-loadingPaper`, - chips: `${PREFIX}-chips`, - newUser: `${PREFIX}-newUser`, - appBarDark: `${PREFIX}-appBarDark`, - appBarLight: `${PREFIX}-appBarLight`, - labeltxtColor: `${PREFIX}-labeltxtColor`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - margin: '10px 5px', - }, - [`& .${classes.userContainer}`]: { - display: 'flex', - justifyContent: 'space-between', - flexDirection: 'row', - marginBottom: 10, - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - padding: 20, - margin: 0, - paddingBottom: 10, - }, - }, - [`& .${classes.userIcon}`]: { - display: 'flex', - marginRight: 10, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - borderRadius: '5px', - minheight: '39vh', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - flexDirection: 'column', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - marginRight: 0, - width: '100%', - }, - }, - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - height: '200px', - width: '200px', - }, - [`& .${classes.userDetails}`]: { - display: 'flex', - width: '100%', - justifyContent: 'flex-start', - alignItems: 'center', - padding: '20px', - borderRadius: '5px', - minheight: '39vh', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - flexDirection: 'row', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column-reverse', - alignItems: 'start', - marginTop: 10, - }, - [theme.breakpoints.down('md')]: { - justifyContent: 'space-between', - }, - }, - [`& .${classes.loadingLine}`]: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - width: '50%', - [theme.breakpoints.down('md')]: { - width: '100%', - }, - }, - [`& .${classes.tagsContainer}`]: { - display: 'flex', - alignItems: 'center', - marginTop: 3, - }, - [`& .${classes.loadingPaper}`]: { - display: 'flex', - justifyContent: 'center', - padding: 20, - borderRadius: 5, - minheight: '39vh', - marginBottom: 10, - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - flexDirection: 'column', - alignItems: 'center', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - }, - }, - [`& .${classes.chips}`]: { - background: '#ffffff70', - margin: 2, - }, - [`& .${classes.newUser}`]: { - margin: 'auto', - color: 'white', - }, - [`& .${classes.appBarDark}`]: { - backgroundColor: 'rgba(5, 5, 5, .90)', - }, - [`& .${classes.appBarLight}`]: { - color: 'black', - }, - [`& .${classes.labeltxtColor}`]: { - color: 'white', - }, -})) - -export default Root \ No newline at end of file diff --git a/client/src/components/User/TabPage/index.jsx b/client/src/components/User/TabPage/index.jsx deleted file mode 100644 index 22e6d736..00000000 --- a/client/src/components/User/TabPage/index.jsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Root, classes } from './styles' -import { Typography, Grid, Pagination, Skeleton } from '@mui/material' -import { LoadingCard, PostCard, CommentCard, CommentLoadingCard } from '../Cards' - -const TabPage = (props) => { - const { posts, numberOfPages, isLoading, notDoneText, page, setPage, userId, comments } = props - - return ( - -
-
- {isLoading ? ( - - {posts ? [...Array(10).keys()].map((key) => ) : [...Array(2).keys()].map((key) => )} - - ) : posts?.length ? ( - - {posts.map((post) => ( - - ))} - - ) : comments?.length ? ( - - {comments.map((comment) => ( - - ))} - - ) : ( - - {notDoneText} - - )} -
- {isLoading ? : setPage(newPage)} />} -
-
- ) -} - -export default TabPage diff --git a/client/src/components/User/TabPage/styles.js b/client/src/components/User/TabPage/styles.js deleted file mode 100644 index 94e7e7c8..00000000 --- a/client/src/components/User/TabPage/styles.js +++ /dev/null @@ -1,42 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'TabPage' -export const classes = { - root: `${PREFIX}-root`, - loadingPaper: `${PREFIX}-loadingPaper`, - container: `${PREFIX}-container`, - noPostsLiked: `${PREFIX}-noPostsLiked`, - tab: `${PREFIX}-tab`, - -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: {}, - [`& .${classes.tab}`]: { - display: 'flex', - justifyContent: 'center', - minheight: '39vh', - flexDirection: 'column', - alignItems: 'center', - width: '100%', - [theme.breakpoints.down('sm')]: { - flexDirection: 'column', - alignItems: 'center', - }, - }, - [`& .${classes.container}`]: { - justifyContent: 'space-around', - marginTop: 10, - marginLeft: 0, - flexDirection: 'row', - width: '100%', - }, - [`& .${classes.noPostsLiked}`]: { - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - color: 'white', - }, -})) - -export default Root diff --git a/client/src/components/User/Update/index.jsx b/client/src/components/User/Update/index.jsx deleted file mode 100644 index c502b967..00000000 --- a/client/src/components/User/Update/index.jsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState, useEffect, useContext } from 'react' -import { Button, Paper, Grid, Typography, Container } from '@mui/material' -import { Shuffle } from '@mui/icons-material' -import { useDispatch } from 'react-redux' -import { useNavigate } from 'react-router-dom' -import { Root, classes } from './styles' -import { updateUser } from '../../../actions/user' -import Input from '../../Input' -import { RandomAvatar } from '../../UserIcon/avatar' -import lodash from 'lodash' -import Avatar from 'avataaars2' -import { SnackbarContext } from '../../../contexts/SnackbarContext' - -const Update = ({ user, setUser }) => { - const { openSnackBar: snackBar } = useContext(SnackbarContext) - const history = useNavigate() - const dispatch = useDispatch() - const [showPassword, setShowPassword] = useState(false) - const handleShowPassword = () => setShowPassword((prevShowPassword) => !prevShowPassword) - const initialState = { - firstName: user.result.name.split(' ')[0], - lastName: user.result.name.split(' ')[1], - avatar: user.result.avatar, - email: user.result.email, - id: user.result._id - } - const [formData, setFormData] = useState(initialState) - const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) - const [avatar, setAvatar] = useState(formData.avatar) - - const shuffle = () => setAvatar(RandomAvatar()) - useEffect(() => { - setAvatar(avatar) - setFormData({ ...formData, avatar }) - }, [avatar]) - - const handleSubmit = (e) => { - e.preventDefault() - dispatch(updateUser(formData, history, setUser, snackBar)) - } - - const prefillData = () => { - setAvatar(initialState.avatar) - setFormData(initialState) - } - return ( - - - - - - Update Details -
- - - - - - - - - - -
-
-
-
- ) -} - -export default Update diff --git a/client/src/components/User/Update/styles.js b/client/src/components/User/Update/styles.js deleted file mode 100644 index 8c7b8f58..00000000 --- a/client/src/components/User/Update/styles.js +++ /dev/null @@ -1,48 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'Update' -export const classes = { - root: `${PREFIX}-root`, - paper: `${PREFIX}-paper`, - avatar: `${PREFIX}-avtar`, - form: `${PREFIX}-form`, - submit: `${PREFIX}-submit`, - googleButton: `${PREFIX}-googleButton`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - '& .MuiFormLabel-root': { - color: 'white', - }, - }, - [`& .${classes.paper}`]: { - marginBottom: theme.spacing(3), - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: theme.spacing(2), - transition: '0.2s', - backgroundColor: 'rgba(255, 255, 255, .09)', - backdropFilter: 'blur(10px)', - borderRadius: '5px', - }, - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - height: '200px', - width: '200px', - borderRadius: '200px', - }, - [`& .${classes.form}`]: { - width: '100%', // Fix IE 11 issue. - marginTop: theme.spacing(3), - }, - [`& .${classes.submit}`]: { - margin: theme.spacing(1, 0, 0, 0), - }, - [`& .${classes.googleButton}`]: { - marginBottom: theme.spacing(2), - }, -})) - -export default Root diff --git a/client/src/components/UserAvatar.jsx b/client/src/components/UserAvatar.jsx new file mode 100644 index 00000000..284304b1 --- /dev/null +++ b/client/src/components/UserAvatar.jsx @@ -0,0 +1,15 @@ +import { Avatar, IconButton, Tooltip } from '@mui/material' + +const UserAvatar = ({ onClick: handleClick, user, tooltipText, disableHover, size }) => { + return ( + + + + {user.fullName.charAt(0)} + + + + ) +} + +export default UserAvatar diff --git a/client/src/components/UserIcon/UserIcon.jsx b/client/src/components/UserIcon/UserIcon.jsx deleted file mode 100644 index ba214450..00000000 --- a/client/src/components/UserIcon/UserIcon.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { useState, useEffect } from 'react' -import { Button } from '@mui/material' -import { Root, classes } from './styles' -import ShuffleIcon from '@mui/icons-material/Shuffle' -import Avatar from 'avataaars2' -import { RandomAvatar } from './avatar' - -export const UserIcon = ({ formData, setFormData }) => { - const [avatar, setAvatar] = useState(RandomAvatar()) - - const shuffle = () => setAvatar(RandomAvatar()) - useEffect(() => { - setAvatar(avatar) - setFormData({ ...formData, avatar: avatar }) - }, [avatar]) - - return ( - - - - - ) -} - -export default UserIcon diff --git a/client/src/components/UserIcon/avatar.js b/client/src/components/UserIcon/avatar.js deleted file mode 100644 index 25a165e1..00000000 --- a/client/src/components/UserIcon/avatar.js +++ /dev/null @@ -1,30 +0,0 @@ -const configs = { - topType: ['Eyepatch', 'Hat', 'Hijab', 'Turban', 'WinterHat1', 'WinterHat2', 'WinterHat3', 'WinterHat4', 'LongHairBigHair', 'LongHairBob', 'LongHairBun', 'LongHairCurly', 'LongHairCurvy', 'LongHairDreads', 'LongHairFrida', 'LongHairFro', 'LongHairFroBand', 'LongHairNotTooLong', 'LongHairShavedSides', 'LongHairMiaWallace', 'LongHairStraight', 'LongHairStraight2', 'LongHairStraightStrand', 'ShortHairDreads01', 'ShortHairDreads02'], - accessoriesType: ['Blank', 'Kurt', 'Prescription01', 'Prescription02', 'Round', 'Sunglasses', 'Wayfarers'], - hatColor: ['Black', 'Blue01', 'Blue02', 'Blue03', 'Gray01', 'Gray02', 'Heather', 'PastelBlue', 'PastelGreen', 'PastelOrange', 'PastelRed', 'PastelYellow', 'Pink', 'Red', 'White'], - hairColor: ['Auburn', 'Black', 'Blonde', 'BlondeGolden', 'Brown', 'BrownDark', 'PastelPink', 'Platinum', 'Red', 'SilverGray'], - facialHairType: ['Blank', 'BeardMedium', 'BeardLight', 'BeardMajestic', 'MoustacheFancy', 'MoustacheMagnum'], - facialHairColor: ['Auburn', 'Black', 'Blonde', 'BlondeGolden', 'Brown', 'BrownDark', 'Platinum', 'Red'], - clotheType: ['BlazerShirt', 'BlazerSweater', 'CollarSweater', 'GraphicShirt', 'Hoodie', 'Overall', 'ShirtCrewNeck', 'ShirtScoopNeck', 'ShirtVNeck'], - clotheColor: ['Black', 'Blue01', 'Blue02', 'Blue03', 'Gray01', 'Gray02', 'Heather', 'PastelBlue', 'PastelGreen', 'PastelOrange', 'PastelRed', 'PastelYellow', 'Pink', 'Red', 'White'], - graphicType: ['Bat', 'Cumbia', 'Deer', 'Diamond', 'Hola', 'Pizza', 'Resist', 'Selena', 'Bear', 'SkullOutline', 'Skull'], - eyeType: ['Close', 'Default', 'Happy', 'Hearts', 'Side', 'Squint', 'Surprised', 'Wink', 'WinkWacky'], - eyebrowType: ['Default', 'DefaultNatural', 'FlatNatural', 'RaisedExcited', 'RaisedExcitedNatural', 'SadConcerned', 'SadConcernedNatural', 'UnibrowNatural', 'UpDown', 'UpDownNatural'], - mouthType: ['Concerned', 'Default', 'Eating', 'Sad', 'Smile', 'Tongue', 'Twinkle'], - skinColor: ['Tanned', 'Yellow', 'Pale', 'Light'], -} - -const configsKeys = Object.keys(configs) - -export const RandomAvatar = () => { - const options = {} - const keys = [...configsKeys] - keys.forEach((key) => { - const configArray = configs[key] - options[key] = configArray[Math.floor(Math.random() * configArray.length)] - }) - - const top = options.topType - if (top.slice(0, 4) === 'Long' || top === 'Hijab' || top === 'WinterHat2' || options.clotheType === 'ShirtVNeck') options.facialHairType = 'Blank' - return options -} diff --git a/client/src/components/UserIcon/styles.js b/client/src/components/UserIcon/styles.js deleted file mode 100644 index 5aafa920..00000000 --- a/client/src/components/UserIcon/styles.js +++ /dev/null @@ -1,22 +0,0 @@ -import { styled } from '@mui/material/styles' - -const PREFIX = 'UserIcon' - -export const classes = { - root: `${PREFIX}-root`, - avatar: `${PREFIX}-avatar`, -} - -export const Root = styled('div')(({ theme }) => ({ - [`&.${classes.root}`]: { - display: "contents" - }, - [`& .${classes.avatar}`]: { - margin: theme.spacing(1), - height: "200px", - width: "200px", - borderRadius: "200px" - }, -})) - -export default Root diff --git a/client/src/components/data/comments.js b/client/src/components/data/comments.js deleted file mode 100644 index 1ea1fa80..00000000 --- a/client/src/components/data/comments.js +++ /dev/null @@ -1,170 +0,0 @@ -const creator1 = { - _id: '640826f38431da1e123456778', - name: 'John Doe', - email: 'johnDoe@email.com', - avatar: { - accessoriesType: "Round", - clotheColor: "PastelYellow", - clotheType: "GraphicShirt", - eyeType: "WinkWacky", - eyebrowType: "DefaultNatural", - facialHairColor: "BrownDark", - facialHairType: "BeardMedium", - graphicType: "Deer", - hairColor: "Black", - hatColor: "Gray02", - mouthType: "Twinkle", - skinColor: "Yellow", - topType: "WinterHat3" - }, __v: 0 -} - -export const comments = [ - { - "_id": "35fcb842-6bc4-4833-8baf-386efcd270af", - "createdAt": "2023-06-28T22:41:04.897Z", - "creator": creator1, - "likes": [], - "message": "Aperiam asperiores asperiores nulla beatae unde deleniti similique eos.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "252f1bf5-94d0-47f0-b7e5-7d54339b123b", - "createdAt": "2023-07-05T23:44:50.112Z", - "creator": creator1, - "likes": [], - "message": "Nobis dolor necessitatibus unde delectus blanditiis laudantium nostrum.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "db6d688e-5675-4a5e-a1b9-aa487d705533", - "createdAt": "2022-11-18T14:14:54.056Z", - "creator": creator1, - "likes": [], - "message": "Quam assumenda blanditiis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "23fb40b4-2fc6-48df-a6a2-b6014c425fad", - "createdAt": "2023-08-02T13:57:06.790Z", - "creator": creator1, - "likes": [], - "message": "Aut soluta incidunt quasi et minus maiores pariatur.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "14256fad-29b7-43cf-9a26-b57299ec375c", - "createdAt": "2023-02-11T11:25:59.717Z", - "creator": creator1, - "likes": [], - "message": "Eum molestiae dolores beatae nam.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "db840948-e418-4be0-94bf-003e52b90c89", - "createdAt": "2023-06-16T04:53:53.451Z", - "creator": creator1, - "likes": [], - "message": "Aliquam ab sequi adipisci consectetur quis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "b297f3f9-146f-4a80-b573-ba61b973cce4", - "createdAt": "2022-09-03T17:13:20.279Z", - "creator": creator1, - "likes": [], - "message": "Magni modi quibusdam ab provident.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "5af4f936-3d2d-452c-a9fb-22883f00c5d7", - "createdAt": "2022-10-12T16:56:31.227Z", - "creator": creator1, - "likes": [], - "message": "Perspiciatis officiis sit incidunt quas dignissimos cumque fugiat cupiditate facilis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "ca6daa2b-32dd-48c0-988c-613377141d82", - "createdAt": "2023-06-30T15:07:26.501Z", - "creator": creator1, - "likes": [], - "message": "Quos magnam sapiente occaecati aut voluptatum iusto voluptatum.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "994eb70f-4928-4207-91c5-c54ed39e2f0a", - "createdAt": "2023-04-19T14:59:41.258Z", - "creator": creator1, - "likes": [], - "message": "Architecto ipsum sequi consectetur dolore officia minus.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "caeb3fc4-90d3-4ae9-8dae-8b3766e22d2d", - "createdAt": "2023-03-17T13:09:19.624Z", - "creator": creator1, - "likes": [], - "message": "Aliquam voluptate officiis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "1a1abfd0-1fbe-4b92-850d-3baf09ca673d", - "createdAt": "2023-02-22T23:38:15.369Z", - "creator": creator1, - "likes": [], - "message": "At facilis error beatae at distinctio quos necessitatibus quia.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "e30b4a4e-a3ae-4898-ae54-60e898b1426f", - "createdAt": "2023-07-13T01:21:56.148Z", - "creator": creator1, - "likes": [], - "message": "Illum repellendus minus tempore.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "8da95c55-c2be-4c36-94b5-c3c6fda10886", - "createdAt": "2023-04-08T11:22:28.102Z", - "creator": creator1, - "likes": [], - "message": "Sequi consequuntur aperiam vel id ad.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "e40724b4-72a8-4c67-a170-b6595b9a5cd4", - "createdAt": "2023-01-17T00:12:59.369Z", - "creator": creator1, - "likes": [], - "message": "Quia tempore necessitatibus vero laborum fugit odit facilis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - }, - { - "_id": "58afd2a6-b5bd-44e1-bdc7-ed79043d9217", - "createdAt": "2023-07-24T03:21:35.443Z", - "creator": creator1, - "likes": [], - "message": "Commodi optio eum rem illum temporibus numquam debitis.", - "post": "64beed3d7ba580756e0c5fac", - "__v": 0 - } -] -export const pages = 2 -export const page1 = comments.slice(0, 9) -export const page2 = comments.slice(10,) diff --git a/client/src/components/index.js b/client/src/components/index.js new file mode 100644 index 00000000..4b376ca4 --- /dev/null +++ b/client/src/components/index.js @@ -0,0 +1,23 @@ +export { default as AppRouter } from './AppRouter' +export { default as Navbar } from './Navbar' +export { default as ThemeSwitch } from './ThemeSwitch' +export { default as PostCard } from './PostCard' +export { default as AccountMenu } from './AccountMenu' +export { default as ScrollToTop } from './ScrollToTop' +export { default as Snackbar } from './Snackbar' +export { default as Bottombar } from './Bottombar' +export { default as SuspenseFallback } from './SuspenseFallback' +export { default as CreatePostDialog } from './CreatePostDialog' +export { default as Searchbar } from './Searchbar' +export { default as Sidebar } from './Sidebar' +export { default as UserAvatar } from './UserAvatar' +export { default as PostCardSkeleton } from './PostCardSkeleton' +export { default as PostSkeleton } from './PostSkeleton' +export { default as OAuthButtons } from './OAuthButtons' +export { + // + CreateComment as CreateCommentForm, + CreatePost as CreatePostForm, + Search as SearchForm, + UpdateProfile as UpdateProfileForm +} from './Forms' diff --git a/client/src/constants/actionTypes.js b/client/src/constants/actionTypes.js deleted file mode 100644 index b8f9b300..00000000 --- a/client/src/constants/actionTypes.js +++ /dev/null @@ -1,35 +0,0 @@ -export const FETCH_ALL = 'FETCH_ALL' -export const FETCH_POST = 'FETCH_POST' -export const FETCH_BY_SEARCH = 'FETCH_BY_SEARCH' -export const FETCH_LIKED = 'FETCH_LIKED' -export const FETCH_RECOMMENDED = 'FETCHED_RECOMMENDED' -export const FETCH_CREATED = 'FETCH_CREATED' -export const FETCH_PRIVATE = 'FETCH_PRIVATE' -export const FETCH_COMMENTS = 'FETCH_COMMENT' -export const CREATE_COMMENT = 'CREATE_COMMENT' -export const USER_DETAILS = 'USER_DETAILS' -export const CREATE = 'CREATE' -export const UPDATE = 'UPDATE' -export const DELETE = 'DELETE' -export const DELETE_COMMENT = 'DELETE_COMMENT' -export const AUTH = 'AUTH' -export const LOGOUT = 'LOGOUT' -export const COMMENT = 'COMMENT' -export const START_LOADING = 'START_LOADING' -export const END_LOADING = 'END_LOADING' - -export const DELETING_POST = 'DELETING_POST' -export const CREATING_POST = 'CREATING_POST' -export const FETCHING_LIKED_POSTS = 'FETCHING_LIKED_POSTS' -export const FETCHING_CREATED_POSTS = 'FETCHING_CREATED_POSTS' -export const FETCHING_RECOMMENDED_POSTS = 'FETCHING_RECOMMENDED_POSTS' -export const FETCHING_COMMENTS = 'FETCHING_COMMENTS' -export const FETCHING_PRIVATE_POSTS = 'FETCHING_PRIVATE_POSTS' - -export const DELETED_POST = 'DELETED_POST' -export const CREATED_POST = 'CREATED_POST' -export const FETCHED_LIKED_POSTS = 'FETCHED_LIKED_POSTS' -export const FETCHED_CREATED_POSTS = 'FETCHED_CREATED_POSTS' -export const FETCHED_PRIVATE_POSTS = 'FETCHED_PRIVATE_POSTS' -export const FETCHED_RECOMMENDED_POSTS = 'FETCHED_RECOMMENDED_POSTS' -export const FETCHED_COMMENTS = 'FETCHED_COMMENT' \ No newline at end of file diff --git a/client/src/contexts/ModeContext.js b/client/src/contexts/ModeContext.js deleted file mode 100644 index c3b5ae04..00000000 --- a/client/src/contexts/ModeContext.js +++ /dev/null @@ -1,10 +0,0 @@ -// USING CONTEXT TO PROVIDE THEME FEATURE GLOBALLY - -import { createContext } from "react"; - -export const modes = { - dark: 'dark', - light: 'light', -}; - -export const ModeContext = createContext(); diff --git a/client/src/contexts/SnackbarContext.jsx b/client/src/contexts/SnackbarContext.jsx deleted file mode 100644 index 0bbe6b4c..00000000 --- a/client/src/contexts/SnackbarContext.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import { createContext, useState } from 'react' - -export const SnackbarContext = createContext() - -export const SnackbarProvider = ({ children }) => { - const [snackBarMessage, setSnackBarMessage] = useState('') - const [alertSeverity, setAlertSeverity] = useState('success') - const [open, setOpen] = useState(false) - - const openSnackBar = (severity, message) => { - setAlertSeverity(severity) - setSnackBarMessage(message) - setOpen(true) - } - - const contextValue = { - openSnackBar, - snackBarProps: { - open, - setOpen, - alertSeverity, - snackBarMessage, - }, - } - - return {children} -} diff --git a/client/src/contexts/index.js b/client/src/contexts/index.js new file mode 100644 index 00000000..bad3ab2c --- /dev/null +++ b/client/src/contexts/index.js @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export const ThemeContext = createContext() diff --git a/client/src/data/comments.js b/client/src/data/comments.js new file mode 100644 index 00000000..852c00b4 --- /dev/null +++ b/client/src/data/comments.js @@ -0,0 +1,15 @@ +export const comments = [ + { + id: 1, + author: 'Alice Johnson', + content: 'Great post! Very informative.', + likes: 5, + avatar: 'https://picsum.photos/seed/1/200' + }, + { id: 2, author: 'Bob Smith', content: 'I have a question about the third point. Can you elaborate?', likes: 3, avatar: 'https://picsum.photos/seed/2/200' }, + { id: 3, author: 'Charlie Brown', content: 'Thanks for sharing this information!', likes: 7, avatar: 'https://picsum.photos/seed/3/200' }, + { id: 4, author: 'Diana Prince', content: 'This helped me understand the topic better.', likes: 2, avatar: 'https://picsum.photos/seed/4/200' }, + { id: 5, author: 'Ethan Hunt', content: 'Looking forward to more posts like this!', likes: 4, avatar: 'https://picsum.photos/seed/5/200' }, + { id: 6, author: 'Fiona Apple', content: 'Interesting perspective on the subject.', likes: 6, avatar: 'https://picsum.photos/seed/6/200' }, + { id: 7, author: 'George Lucas', content: "I'd love to see more examples in future posts.", likes: 1, avatar: 'https://picsum.photos/seed/7/200' } +] diff --git a/client/src/data/index.js b/client/src/data/index.js new file mode 100644 index 00000000..8cf8dee7 --- /dev/null +++ b/client/src/data/index.js @@ -0,0 +1,22 @@ +import { comments } from './comments' +import { movies } from './movies' +import { posts } from './posts' + +import { default as users } from './users' + +export const pages = 2 +export const page1 = comments.slice(0, 9) +export const page2 = comments.slice(10) +// Mock user data (replace with actual data fetching logic) +const user = { + metrics: { + postsCount: 42, + longestPostWords: 1500, + longestPostId: 'abc123', + likesReceived: 256, + commentsReceived: 128, + privatePostsCount: 5 + } +} + +export { comments, movies, users, posts, user } diff --git a/client/src/data/movies.js b/client/src/data/movies.js new file mode 100644 index 00000000..95664be0 --- /dev/null +++ b/client/src/data/movies.js @@ -0,0 +1,126 @@ +export const movies = [ + { title: 'The Shawshank Redemption', year: 1994 }, + { title: 'The Godfather', year: 1972 }, + { title: 'The Godfather: Part II', year: 1974 }, + { title: 'The Dark Knight', year: 2008 }, + { title: '12 Angry Men', year: 1957 }, + { title: "Schindler's List", year: 1993 }, + { title: 'Pulp Fiction', year: 1994 }, + { + title: 'The Lord of the Rings: The Return of the King', + year: 2003 + }, + { title: 'The Good, the Bad and the Ugly', year: 1966 }, + { title: 'Fight Club', year: 1999 }, + { + title: 'The Lord of the Rings: The Fellowship of the Ring', + year: 2001 + }, + { + title: 'Star Wars: Episode V - The Empire Strikes Back', + year: 1980 + }, + { title: 'Forrest Gump', year: 1994 }, + { title: 'Inception', year: 2010 }, + { + title: 'The Lord of the Rings: The Two Towers', + year: 2002 + }, + { title: "One Flew Over the Cuckoo's Nest", year: 1975 }, + { title: 'Goodfellas', year: 1990 }, + { title: 'The Matrix', year: 1999 }, + { title: 'Seven Samurai', year: 1954 }, + { + title: 'Star Wars: Episode IV - A New Hope', + year: 1977 + }, + { title: 'City of God', year: 2002 }, + { title: 'Se7en', year: 1995 }, + { title: 'The Silence of the Lambs', year: 1991 }, + { title: "It's a Wonderful Life", year: 1946 }, + { title: 'Life Is Beautiful', year: 1997 }, + { title: 'The Usual Suspects', year: 1995 }, + { title: 'LΓ©on: The Professional', year: 1994 }, + { title: 'Spirited Away', year: 2001 }, + { title: 'Saving Private Ryan', year: 1998 }, + { title: 'Once Upon a Time in the West', year: 1968 }, + { title: 'American History X', year: 1998 }, + { title: 'Interstellar', year: 2014 }, + { title: 'Casablanca', year: 1942 }, + { title: 'City Lights', year: 1931 }, + { title: 'Psycho', year: 1960 }, + { title: 'The Green Mile', year: 1999 }, + { title: 'The Intouchables', year: 2011 }, + { title: 'Modern Times', year: 1936 }, + { title: 'Raiders of the Lost Ark', year: 1981 }, + { title: 'Rear Window', year: 1954 }, + { title: 'The Pianist', year: 2002 }, + { title: 'The Departed', year: 2006 }, + { title: 'Terminator 2: Judgment Day', year: 1991 }, + { title: 'Back to the Future', year: 1985 }, + { title: 'Whiplash', year: 2014 }, + { title: 'Gladiator', year: 2000 }, + { title: 'Memento', year: 2000 }, + { title: 'The Prestige', year: 2006 }, + { title: 'The Lion King', year: 1994 }, + { title: 'Apocalypse Now', year: 1979 }, + { title: 'Alien', year: 1979 }, + { title: 'Sunset Boulevard', year: 1950 }, + { + title: 'Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb', + year: 1964 + }, + { title: 'The Great Dictator', year: 1940 }, + { title: 'Cinema Paradiso', year: 1988 }, + { title: 'The Lives of Others', year: 2006 }, + { title: 'Grave of the Fireflies', year: 1988 }, + { title: 'Paths of Glory', year: 1957 }, + { title: 'Django Unchained', year: 2012 }, + { title: 'The Shining', year: 1980 }, + { title: 'WALLΒ·E', year: 2008 }, + { title: 'American Beauty', year: 1999 }, + { title: 'The Dark Knight Rises', year: 2012 }, + { title: 'Princess Mononoke', year: 1997 }, + { title: 'Aliens', year: 1986 }, + { title: 'Oldboy', year: 2003 }, + { title: 'Once Upon a Time in America', year: 1984 }, + { title: 'Witness for the Prosecution', year: 1957 }, + { title: 'Das Boot', year: 1981 }, + { title: 'Citizen Kane', year: 1941 }, + { title: 'North by Northwest', year: 1959 }, + { title: 'Vertigo', year: 1958 }, + { + title: 'Star Wars: Episode VI - Return of the Jedi', + year: 1983 + }, + { title: 'Reservoir Dogs', year: 1992 }, + { title: 'Braveheart', year: 1995 }, + { title: 'M', year: 1931 }, + { title: 'Requiem for a Dream', year: 2000 }, + { title: 'AmΓ©lie', year: 2001 }, + { title: 'A Clockwork Orange', year: 1971 }, + { title: 'Like Stars on Earth', year: 2007 }, + { title: 'Taxi Driver', year: 1976 }, + { title: 'Lawrence of Arabia', year: 1962 }, + { title: 'Double Indemnity', year: 1944 }, + { + title: 'Eternal Sunshine of the Spotless Mind', + year: 2004 + }, + { title: 'Amadeus', year: 1984 }, + { title: 'To Kill a Mockingbird', year: 1962 }, + { title: 'Toy Story 3', year: 2010 }, + { title: 'Logan', year: 2017 }, + { title: 'Full Metal Jacket', year: 1987 }, + { title: 'Dangal', year: 2016 }, + { title: 'The Sting', year: 1973 }, + { title: '2001: A Space Odyssey', year: 1968 }, + { title: "Singin' in the Rain", year: 1952 }, + { title: 'Toy Story', year: 1995 }, + { title: 'Bicycle Thieves', year: 1948 }, + { title: 'The Kid', year: 1921 }, + { title: 'Inglourious Basterds', year: 2009 }, + { title: 'Snatch', year: 2000 }, + { title: '3 Idiots', year: 2009 }, + { title: 'Monty Python and the Holy Grail', year: 1975 } +] diff --git a/client/src/data/posts.js b/client/src/data/posts.js new file mode 100644 index 00000000..6f3d4baa --- /dev/null +++ b/client/src/data/posts.js @@ -0,0 +1,13 @@ +export const posts = [ + { + id: '64beed3d7ba580756e0c5fac', + creator: '640826f38431da1e123456778', + title: 'Post Title', + message: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.', + thumbnail: 'https://source.unsplash.com/random/800x600', + likes: ['1'], + createdAt: '2021-09-01T12:34:56.000Z', + updatedAt: '2021-09-01T12:34:56.000Z', + comments: 1 + } +] diff --git a/client/src/data/users.js b/client/src/data/users.js new file mode 100644 index 00000000..f6e3b2b0 --- /dev/null +++ b/client/src/data/users.js @@ -0,0 +1,21 @@ +export const user = { + _id: '640826f38431da1e123456778', + name: 'John Doe', + firstName: 'Jane', + lastName: 'Doe', + email: 'johnDoe@email.com', + bio: 'Passionate writer and tech enthusiast.', + image: 'https://mui.com/static/images/avatar/3.jpg', + created_at: Date.now(), + __v: 0 +} + +export const user2 = { + _id: '640826f38431da1e123456778', + name: 'John Doe', + email: 'johnDoe@email.com', + __v: 0 +} +const users = { user, user2 } + +export default users diff --git a/client/src/hooks.js b/client/src/hooks.js deleted file mode 100644 index d850f78e..00000000 --- a/client/src/hooks.js +++ /dev/null @@ -1,56 +0,0 @@ -import { useRef, useEffect } from 'react' - -/** - * Returns the previous value of the state of a component - * @param {any} Current value in component state - * @returns Previous value in component state - */ -export const usePrevious = (value) => { - - const ref = useRef(); - useEffect(() => { - ref.current = value; - }); - return ref.current; -}; - -/** - * Helper function to delay the program a number of ms - * @param {Number} ms Wait time in ms - * @returns a promise that is resolved after the time has passed - */ -const sleep = (ms) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Adds functionality to Swipeable component to be used with tabs - * @param {RefObject} swipeableViewsRef Swipeable ref object - * @param {Number} idx current index (zero based) - */ -export const useSwipe = (swipeableViewsRef, idx) => { - const prevIdx = usePrevious(idx) - const swipeForward = () => { - swipeableViewsRef.current.swipeForward() - }; - - const swipeBackward = () => { - swipeableViewsRef.current.swipeBackward() - }; - - useEffect(() => { - ;(async function () { - if (prevIdx < idx) { - for (let i = 0; i < idx - prevIdx; i++) { - swipeForward(); - await sleep(300); - } - } else if (idx < prevIdx) { - for (let i = 0; i < prevIdx - idx; i++) { - swipeBackward() - await sleep(300); - } - } - })() - }, [idx, prevIdx]); -} diff --git a/client/src/hooks/index.js b/client/src/hooks/index.js new file mode 100644 index 00000000..9244414c --- /dev/null +++ b/client/src/hooks/index.js @@ -0,0 +1,14 @@ +import { useContext } from 'react' +import { ThemeContext } from '@/contexts' + +export const useTheme = () => { + const context = useContext(ThemeContext) + + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider') + } + + return context +} + +export * from './posts' diff --git a/client/src/hooks/posts.js b/client/src/hooks/posts.js new file mode 100644 index 00000000..2164934a --- /dev/null +++ b/client/src/hooks/posts.js @@ -0,0 +1,250 @@ +import { useEffect, useState } from 'react' +import { createPost, deletePost, getPost, getPosts, reactPost, searchPosts, unreactPost, updatePost } from '@/api' +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { useStore } from '@/store' +import { useUser } from '@clerk/clerk-react' + +export const useGetPosts = () => { + const { setPages } = useStore() + + const query = useInfiniteQuery({ + queryKey: ['posts'], + queryFn: ({ pageParam }) => getPosts(pageParam, 10), + getNextPageParam: (lastPage) => lastPage.nextCursor + }) + + useEffect(() => { + if (query.data?.pages) { + setPages(query.data.pages) + } + }, [query.data?.pages, setPages]) + + return query +} + +export const useGetPost = (id) => { + return useQuery({ + queryKey: ['post', id], + queryFn: () => getPost(id) + }) +} + +export const useCreatePost = () => { + const queryClient = useQueryClient() + const { pages, setPages } = useStore() + const { user } = useUser() + return useMutation({ + mutationFn: createPost, + onMutate: async (newPost) => { + await queryClient.cancelQueries({ queryKey: ['posts'] }) + const previousData = queryClient.getQueryData(['posts']) + + const optimisticPost = { + ...newPost, + imageUrl: newPost.media, + id: Date.now(), + tags: newPost.tags.map((tag) => ({ tag: { name: tag } })), + reactions: [], + author: { + fullName: user.fullName, + imageUrl: user.imageUrl + }, + reactionCount: 0 + } + const updatedPages = [ + { + posts: [optimisticPost], + nextCursor: pages[0]?.nextCursor + }, + ...pages + ] + + queryClient.setQueryData(['posts'], (old) => ({ + ...(old ?? { pageParams: [] }), + pages: updatedPages + })) + setPages(updatedPages) + + return { previousData } + }, + onError: (_err, _newPost, context) => { + const previousPages = context?.previousData?.pages ?? [] + queryClient.setQueryData(['posts'], context?.previousData) + setPages(previousPages) + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }) + }) +} + +export const useUpdatePost = () => { + const queryClient = useQueryClient() + const { pages, setPages } = useStore() + + return useMutation({ + mutationFn: (post) => updatePost(post.id, post), + onMutate: async (updatedPost) => { + await queryClient.cancelQueries({ queryKey: ['posts'] }) + const previousData = queryClient.getQueryData(['posts']) + + // Update both states + const newPages = pages.map((page) => ({ + ...page, + posts: page.posts.map((post) => (post.id === updatedPost.id ? { ...post, ...updatedPost } : post)) + })) + queryClient.setQueryData(['posts'], (old) => ({ + ...(old ?? { pageParams: [] }), + pages: newPages + })) + setPages(newPages) + + return { previousData } + }, + onError: (_err, _updatedPost, context) => { + const previousPages = context?.previousData?.pages ?? [] + queryClient.setQueryData(['posts'], context?.previousData) + setPages(previousPages) + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }) + }) +} + +export const useDeletePost = () => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: deletePost, + onMutate: async (postId) => { + await queryClient.cancelQueries({ queryKey: ['posts'] }) + const previousData = queryClient.getQueryData(['posts']) + + queryClient.setQueryData(['posts'], (old) => { + if (!old) { + return { pages: [], pageParams: [] } + } + return { + ...old, + pages: old.pages.map((page) => ({ + ...page, + posts: page.posts.filter((post) => post.id !== postId) + })) + } + }) + + return { previousData } + }, + onError: (_err, _postId, context) => queryClient.setQueryData(['posts'], context?.previousData), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }) + }) +} + +export const useSearchPosts = () => { + return useQuery({ + queryKey: ['searchPosts'], + queryFn: () => searchPosts + }) +} + +export const useRefresh = () => { + const queryClient = useQueryClient() + const { setPages } = useStore() + const [refreshing, setRefreshing] = useState(false) + + return { + refresh: async () => { + try { + setRefreshing(true) + setPages([]) + + await queryClient.resetQueries({ queryKey: ['posts'] }) + + return true + } catch (error) { + console.error('Failed to refresh:', error) + return false + } finally { + setRefreshing(false) + } + }, + refreshing + } +} + +export const useReactPost = () => { + const queryClient = useQueryClient() + const { pages, setPages } = useStore() + + return useMutation({ + mutationFn: ({ postId, type }) => reactPost(postId, type), + onMutate: async ({ postId, type }) => { + await queryClient.cancelQueries({ queryKey: ['posts'] }) + const previousData = queryClient.getQueryData(['posts']) + + // Update both states with optimistic update + const newPages = pages.map((page) => ({ + ...page, + posts: page.posts.map((post) => { + if (post.id === postId) { + return { + ...post, + reactionCount: post.reactionCount++, + reactions: [...post.reactions, { reactionType: type }] + } + } + return post + }) + })) + + queryClient.setQueryData(['posts'], (old) => ({ + ...(old ?? { pageParams: [] }), + pages: newPages + })) + setPages(newPages) + + return { previousData } + }, + onError: (_err, _variables, context) => { + const previousPages = context?.previousData?.pages ?? [] + queryClient.setQueryData(['posts'], context?.previousData) + setPages(previousPages) + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }) + }) +} + +export const useUnreactPost = () => { + const queryClient = useQueryClient() + const { pages, setPages } = useStore() + return useMutation({ + mutationFn: (postId) => unreactPost(postId), + onMutate: async (postId) => { + await queryClient.cancelQueries({ queryKey: ['posts'] }) + const previousData = queryClient.getQueryData(['posts']) + const newPages = pages.map((page) => ({ + ...page, + posts: page.posts.map((post) => { + if (post.id === postId) { + return { + ...post, + reactionCount: post.reactionCount--, + reactions: [{ reactionType: null }] + } + } + return post + }) + })) + queryClient.setQueryData(['posts'], (old) => ({ + ...(old ?? { pageParams: [] }), + pages: newPages + })) + setPages(newPages) + + return { previousData } + }, + onError: (_err, _postId, context) => { + const previousPages = context?.previousData?.pages ?? [] + queryClient.setQueryData(['posts'], context?.previousData) + setPages(previousPages) + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['posts'] }) + }) +} diff --git a/client/src/images/darkmodeIcon.png b/client/src/images/darkmodeIcon.png deleted file mode 100644 index 1dbff512..00000000 Binary files a/client/src/images/darkmodeIcon.png and /dev/null differ diff --git a/client/src/images/icon.png b/client/src/images/icon.png deleted file mode 100644 index 39f39c28..00000000 Binary files a/client/src/images/icon.png and /dev/null differ diff --git a/client/src/images/lightmodeIcon.png b/client/src/images/lightmodeIcon.png deleted file mode 100644 index 8c4a9449..00000000 Binary files a/client/src/images/lightmodeIcon.png and /dev/null differ diff --git a/client/src/index.css b/client/src/index.css deleted file mode 100644 index cf3945f9..00000000 --- a/client/src/index.css +++ /dev/null @@ -1,30 +0,0 @@ -body { - background-color: #0d1117; - margin: 0 0 0 0; -} - -* { - font-family: Heebo, sans-serif !important; -} - -::-webkit-scrollbar { - width: 10px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: slategrey; - border-radius: 5px; -} - -::-webkit-scrollbar-thumb:hover { - background: skyblue; -} - -/* remove pagination from all swipable containers */ -.swipeable-container .pagination { - display: none; -} \ No newline at end of file diff --git a/client/src/index.jsx b/client/src/index.jsx deleted file mode 100644 index c6934a2d..00000000 --- a/client/src/index.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import ReactDOM from 'react-dom/client' -import { Provider } from 'react-redux' -import { legacy_createStore as createStore, applyMiddleware, compose } from 'redux' -import thunk from 'redux-thunk' -import reducers from './reducers' -import './index.css' -import App from './App/App' -import * as serviceWorkerRegistration from './serviceWorkerRegistration' -import { SnackbarProvider } from './contexts/SnackbarContext' -const root = ReactDOM.createRoot(document.getElementById('root')) -const store = createStore(reducers, {}, compose(applyMiddleware(thunk))) -root.render( - - - - - -) - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://cra.link/PWA -serviceWorkerRegistration.unregister() diff --git a/client/src/lib/index.js b/client/src/lib/index.js new file mode 100644 index 00000000..e69de29b diff --git a/client/src/lib/utils.js b/client/src/lib/utils.js new file mode 100644 index 00000000..f2f46e1c --- /dev/null +++ b/client/src/lib/utils.js @@ -0,0 +1,12 @@ +export const convertToBase64 = (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result) + reader.onerror = (error) => reject(error) + reader.readAsDataURL(file) + }) +} + +export const getThumbnail = (url) => url.replace('/upload/', '/upload/q_10/') + +export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/client/src/main.jsx b/client/src/main.jsx index 703d2def..8ca02bf3 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -1,22 +1,10 @@ -import ReactDOM from 'react-dom/client' -import { Provider } from 'react-redux' -import { legacy_createStore as createStore, applyMiddleware, compose } from 'redux' -import thunk from 'redux-thunk' -import reducers from './reducers' -import './index.css' -import App from './App/App' -import * as serviceWorkerRegistration from './serviceWorkerRegistration' -import { SnackbarProvider } from './contexts/SnackbarContext' +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import App from '@/App' +import '@/styles/index.css' -const store = createStore(reducers, {}, compose(applyMiddleware(thunk))) -ReactDOM.createRoot(document.getElementById('root')).render( - - - - - +createRoot(document.getElementById('root')).render( + + + ) -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://cra.link/PWA -serviceWorkerRegistration.unregister() diff --git a/client/src/pages/LogIn.jsx b/client/src/pages/LogIn.jsx new file mode 100644 index 00000000..1aec3fed --- /dev/null +++ b/client/src/pages/LogIn.jsx @@ -0,0 +1,82 @@ +import { Container, Button, Paper, TextField, Typography, Avatar, Stack, Divider, FormControl, FormHelperText } from '@mui/material' +import { Link, useNavigate } from 'react-router-dom' +import { LockOutlined } from '@mui/icons-material' +import { useSignIn } from '@clerk/clerk-react' +import { OAuthButtons } from '@/components' +import { useState } from 'react' + +const Form = () => { + const initialState = { email: '', password: '' } + const { isLoaded, signIn, setActive } = useSignIn() + const [formData, setFormData] = useState(initialState) + const [error, setError] = useState('') + const navigate = useNavigate() + + const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) + + const handleSubmit = async (event) => { + event.preventDefault() + setError('') + if (!isLoaded) { + return + } + try { + const result = await signIn.create({ + identifier: formData.email, + password: formData.password + }) + + if (result.status === 'complete') { + await setActive({ session: result.createdSessionId }) + navigate('/') + } else { + console.error('Sign-in failed', result) + setError('Sign-in failed. Please try again.') + } + } catch (err) { + setError(err.errors[0].longMessage || 'An error occurred during sign-in') + } + } + + return ( + + + + + WELCOME BACK + + + + + + + + {error} + + + + OR + + + + + ) +} + +const LogIn = () => { + return ( + + + +
+ + + + ) +} + +export default LogIn diff --git a/client/src/pages/NotFound.jsx b/client/src/pages/NotFound.jsx new file mode 100644 index 00000000..31d354b6 --- /dev/null +++ b/client/src/pages/NotFound.jsx @@ -0,0 +1,83 @@ +import { ArrowBackIosNewTwoTone } from '@mui/icons-material' +import { Container, Paper, Typography, Grid2 as Grid, Divider, Button, Stack } from '@mui/material' +import { Link } from 'react-router-dom' + +const NotFoundTypography = () => { + return ( + + 404 + + ) +} + +const NotFoundMessage = () => { + return ( + + + SORRY ! + + The page you are looking for is not found. + + + ) +} + +const NotFound = () => { + return ( + + + + + + + + + + + + + + + + ) +} + +export default NotFound diff --git a/client/src/pages/Post.jsx b/client/src/pages/Post.jsx new file mode 100644 index 00000000..06544cfe --- /dev/null +++ b/client/src/pages/Post.jsx @@ -0,0 +1,180 @@ +import { + // + Avatar, + AvatarGroup, + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + CardMedia, + Chip, + Container, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Grid2 as Grid, + IconButton, + Stack, + TextField, + Tooltip, + Typography +} from '@mui/material' +import { CommentSection, RecommendationSection } from '@/sections' +import { ContentCopy, Facebook, FavoriteOutlined, LinkedIn, Share, X, WhatsApp } from '@mui/icons-material' +import { Link, useNavigate, useParams } from 'react-router-dom' +import { PostSkeleton } from '@/components' +import { useGetPost } from '@/hooks' +import moment from 'moment' +import { useState } from 'react' +import { useStore } from '@/store' + +const AuthorInfo = ({ author, timestamp }) => ( + + + + {author.fullName} + + {author.email} + + + {moment(timestamp).fromNow()} + + + +) + +const users = [ + { id: 1, name: 'John Doe', avatar: 'https://picsum.photos/seed/1/200' }, + { id: 2, name: 'Jane Smith', avatar: 'https://picsum.photos/seed/2/200' }, + { id: 3, name: 'Bob Johnson', avatar: 'https://picsum.photos/seed/3/200' }, + { id: 4, name: 'Alice Brown', avatar: 'https://picsum.photos/seed/4/200' }, + { id: 5, name: 'Charlie Davis', avatar: 'https://picsum.photos/seed/5/200' }, + { id: 6, name: 'Eva Wilson', avatar: 'https://picsum.photos/seed/6/200' } +] + +const PostCard = () => { + const { id } = useParams() + const { data: post, isLoading, error } = useGetPost(id) + const navigate = useNavigate() + const [shareDialogOpen, setShareDialogOpen] = useState(false) + const handleShareClick = () => { + setShareDialogOpen(true) + } + + if (isLoading) { + return + } + if (error?.message === 'API Error: Post not found') { + navigate('/not-found') + } + + return ( + + + } /> + + + + {post.title} + + + {post.tags.map(({ tag: { name } }) => ( + + ))} + + + {post.description} + + + + + + + + + + + + + + {users.map((user) => ( + + + + ))} + + + + setShareDialogOpen(false)} url={window.location.href} /> + + ) +} +const ShareDialog = ({ open, onClose, url }) => { + const shareUrls = { + facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`, + twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}`, + linkedin: `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(url)}`, + whatsapp: `https://api.whatsapp.com/send?text=${encodeURIComponent(url)}` + } + const { openSnackbar } = useStore() + const handleCopy = () => + navigator.clipboard.writeText(url).then(() => { + openSnackbar('Link copied to clipboard', 'info') + }) + + return ( + + Share this post + + + window.open(shareUrls.facebook, '_blank')} aria-label="Share on Facebook"> + + + window.open(shareUrls.twitter, '_blank')} aria-label="Share on Twitter"> + + + window.open(shareUrls.linkedin, '_blank')} aria-label="Share on LinkedIn"> + + + window.open(shareUrls.whatsapp, '_blank')} aria-label="Share on WhatsApp"> + + + + + + + ) + } + }} + /> + + + + + + ) +} +const Post = () => { + return ( + + + + + + + + ) +} + +export default Post diff --git a/client/src/pages/Posts.jsx b/client/src/pages/Posts.jsx new file mode 100644 index 00000000..3c8319fc --- /dev/null +++ b/client/src/pages/Posts.jsx @@ -0,0 +1,69 @@ +import { Box, Container, Grid2 as Grid } from '@mui/material' +import { PostCard, SearchForm, CreatePostForm, Bottombar, PostCardSkeleton } from '@/components' +import { useEffect } from 'react' +import { useGetPosts } from '@/hooks' +import { useInView } from 'react-intersection-observer' +import { useStore } from '@/store' + +const PostGrid = () => { + const { ref, inView } = useInView() + const { pages } = useStore() + const { fetchNextPage, hasNextPage, isFetchingNextPage, status } = useGetPosts() + const allPosts = pages.flatMap((page) => page.posts) + + useEffect(() => { + if (inView && hasNextPage) { + fetchNextPage() + } + }, [inView, hasNextPage, fetchNextPage]) + + if (status === 'pending') { + return Array.from({ length: 6 }).map((_, i) => ( + + + + )) + } + if (status === 'error') { + return Error loading posts: {status} + } + return ( + <> + {allPosts.map((post) => ( + + + + ))} + + {isFetchingNextPage && ( + + + + )} + + + ) +} + +const Posts = () => { + return ( + + + + + + + + + + + + + + + + + ) +} + +export default Posts diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx new file mode 100644 index 00000000..a8b8282f --- /dev/null +++ b/client/src/pages/Profile.jsx @@ -0,0 +1,275 @@ +import { Avatar, Box, Container, Grid2 as Grid, Typography, Paper, List, ListItem, Button, Tab, CircularProgress, Dialog, IconButton, Stack } from '@mui/material' +import { TabContext, TabList, TabPanel } from '@mui/lab' +import { Link, useParams, Navigate } from 'react-router-dom' +import { UpdateProfileForm } from '@/components' +import { useEffect, useState } from 'react' +import { useUser } from '@clerk/clerk-react' +import moment from 'moment' +import { Close } from '@mui/icons-material' + +const UserAvatar = ({ user }) => { + const [open, setOpen] = useState(false) + const handleOpen = () => setOpen(true) + const handleClose = () => setOpen(false) + return ( + <> + + + + + + + + + + + + + + + + + ) +} + +const UserProfileCard = () => { + /** + * TODO: + * - user is not logged in: + * - route is /user -> redirect to /login [handled by AppRouter in PrivateRoute] + * - route is /user/:id -> render other user profile [handled by AppRouter in PrivateRoute] + * - user is logged in: + * - route is /user -> render current user profile + * - route is /user/:id -> render other user profile + * - id is same as user.id -> redirect to /user + * - id is different from user.id -> render other user profile + */ + const { id } = useParams() + const { user, isLoaded, isSignedIn } = useUser() + + // If the user is signed in and the id in the route is the same as the current user's id, + // redirect to /user + if (isSignedIn && id === user.id) { + return + } + + return ( + + + + + + Profile + + + + {!isLoaded ? : (!id && isSignedIn) || id === user?.id ? : } + + + + ) +} + +const CurrentUserProfile = () => { + const { user, isLoaded } = useUser() + const [open, setOpen] = useState(false) + + const handleClose = () => setOpen(false) + + if (!isLoaded) { + return + } + + return ( + <> + + + + {user.fullName} + + + {user.emailAddresses[0].emailAddress} + + + + + {user.unsafeMetadata.bio} + + + + + Joined: {moment(user.createdAt).format('MMMM Do YYYY')} + + + + + + + + ) +} + +const OtherUserProfile = ({ userId }) => { + const [user, setUser] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + // Fetch user data based on userId + // This is a placeholder for actual API call + const fetchUser = () => { + try { + // Replace this with your actual API call + setUser({ + fullName: 'Morty Smith', + email: 'morty.smith@example.com', + bio: 'Aww Jeez, I am so cool!', + imageUrl: 'https://github.com/shadcn.png', + createdAt: new Date('2024-02-14') + }) + // const response = await fetch(`/api/users/${userId}`) + // const data = await response.json() + // setUser(data) + } catch (error) { + console.error('Error fetching user data:', error) + } finally { + setLoading(false) + } + } + + fetchUser() + }, [userId]) + + if (loading) { + return + } + + if (!user) { + return ( + + User not found + + ) + } + + return ( + <> + + + + {user.fullName} + + {user.email} + + + + {user.bio} + + + + + Joined: {moment(user.createdAt).format('MMMM Do YYYY')} + + + + ) +} + +const MetricItem = ({ title, value, isLink, linkTo }) => ( + + {title} + {isLink ? ( + + {value} + + ) : ( + + {value} + + )} + +) + +const UserMetrics = ({ metrics }) => ( + + + + + + Metrics + + + + + + + + + + + + + + +) + +const TabNavigation = ({ value, handleChange }) => ( + + + + + + + No Posts Available + No Posts Available + No Posts Available + +) + +const UserPosts = () => { + const [value, setValue] = useState('liked-posts') + const handleChange = (_, newValue) => setValue(newValue) + + return ( + + + + + + Your Posts + + + + + + + + + ) +} + +const Profile = () => { + const user = { + name: 'John Doe' + } + + return ( + + + + {/* User Profile Card */} + + + {/* Metrics */} + + + {/* Posts */} + + + + + ) +} + +export default Profile diff --git a/client/src/pages/SignUp.jsx b/client/src/pages/SignUp.jsx new file mode 100644 index 00000000..6d30cd53 --- /dev/null +++ b/client/src/pages/SignUp.jsx @@ -0,0 +1,139 @@ +import { Container, Button, Paper, TextField, Typography, Avatar, Grid2 as Grid, Stack, Divider, FormControl, FormHelperText } from '@mui/material' +import { Link, useNavigate } from 'react-router-dom' +import { LockOutlined } from '@mui/icons-material' +import { useSignUp } from '@clerk/clerk-react' +import { OAuthButtons } from '@/components' +import { useState } from 'react' + +const NameFields = ({ formData, errors, handleChange }) => ( + + + + + {errors.firstName} + + + + + + {errors.lastName} + + + +) + +const Form = () => { + const initialState = { firstName: '', lastName: '', email: '', password: '', repeatPassword: '' } + const initialErrorState = { firstName: '', lastName: '', email: '', password: '', repeatPassword: '', clerkError: '' } + const { isLoaded, signUp } = useSignUp() + const [formData, setFormData] = useState(initialState) + const [errors, setErrors] = useState(initialErrorState) + const navigate = useNavigate() + + const handleChange = (e) => setFormData({ ...formData, [e.target.name]: e.target.value }) + + const validateInputs = () => { + const newErrors = { ...initialErrorState } + let valid = true + + // Name validation + const nameRegex = /^[A-Za-z]+$/ + if (!nameRegex.test(formData.firstName)) { + newErrors.firstName = 'Name must contain only letters' + valid = false + } + if (formData.firstName.length < 1) { + newErrors.firstName = 'Name must be at least 1 character' + valid = false + } + if (!nameRegex.test(formData.lastName)) { + newErrors.lastName = 'Name must contain only letters' + valid = false + } + + // Password validation + if (formData.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters long' + valid = false + } + if (formData.password !== formData.repeatPassword) { + newErrors.repeatPassword = 'Passwords do not match' + valid = false + } + + setErrors(newErrors) + return valid + } + + const handleSubmit = async (event) => { + event.preventDefault() + setErrors(initialErrorState) + if (!validateInputs()) { + return + } + if (!isLoaded) { + return + } + try { + await signUp.create({ + firstName: formData.firstName, + lastName: formData.lastName, + emailAddress: formData.email, + password: formData.password + }) + await signUp.prepareVerification({ strategy: 'email_code' }) + navigate('/verify-email') + } catch (error) { + setErrors({ ...initialErrorState, clerkError: error.errors[0].longMessage }) + } + } + + return ( + + + + + JOIN US NOW + + + + {errors.email} + + + + {errors.password} + + + + {errors.repeatPassword} + + + {errors.clerkError} + + + + OR + + + + + ) +} + +const SignUp = () => { + return ( + + + + + + + + ) +} + +export default SignUp diff --git a/client/src/pages/VerifyEmail.jsx b/client/src/pages/VerifyEmail.jsx new file mode 100644 index 00000000..4d0d4688 --- /dev/null +++ b/client/src/pages/VerifyEmail.jsx @@ -0,0 +1,69 @@ +import { Button, TextField, Typography, Paper, Stack, FormControl, FormHelperText, Avatar, Container } from '@mui/material' +import { MailOutlined } from '@mui/icons-material' +import { useSignUp } from '@clerk/clerk-react' +import { useNavigate } from 'react-router-dom' +import { useState } from 'react' + +const Form = () => { + const { isLoaded, signUp, setActive } = useSignUp() + const [code, setCode] = useState('') + const [error, setError] = useState('') + const navigate = useNavigate() + + const handleSubmit = async (event) => { + event.preventDefault() + setError('') + if (!isLoaded) { + return + } + try { + const result = await signUp.attemptEmailAddressVerification({ code }) + + if (result.status === 'complete') { + await setActive({ session: result.createdSessionId }) + navigate('/') + } else { + console.error(JSON.stringify(result, null, 2)) + setError('Sign-up failed. Please try again.') + } + } catch (err) { + console.error('Verification error:', err) + setError(err.errors[0].longMessage || 'An error occurred during verification') + } + } + + const handleChange = (e) => setCode(e.target.value) + + return ( + + + + + Verify Email + Enter the verification code sent to your email. + + + + + {error} + + + + ) +} + +const VerifyEmail = () => { + return ( + + + + + + + + ) +} + +export default VerifyEmail diff --git a/client/src/pages/index.js b/client/src/pages/index.js new file mode 100644 index 00000000..0abaab94 --- /dev/null +++ b/client/src/pages/index.js @@ -0,0 +1,7 @@ +export { default as PostCard } from './Post' +export { default as Posts } from './Posts' +export { default as LogIn } from './LogIn' +export { default as SignUp } from './SignUp' +export { default as Profile } from './Profile' +export { default as NotFound } from './NotFound' +export { default as VerifyEmail } from './VerifyEmail' diff --git a/client/src/providers/Theme.jsx b/client/src/providers/Theme.jsx new file mode 100644 index 00000000..ce4ad886 --- /dev/null +++ b/client/src/providers/Theme.jsx @@ -0,0 +1,43 @@ +import { CssBaseline, ThemeProvider as MUIThemeProvider } from '@mui/material' +import { useEffect } from 'react' +import { ThemeContext } from '@/contexts' +import { Light as lightTheme, Dark as darkTheme } from '@/themes' +import { useStore } from '@/store' + +const ThemeProvider = ({ children }) => { + const { theme, actualTheme, setActualTheme, setThemeAndActual, getCurrentTheme } = useStore() + + useEffect(() => { + const handleSystemThemeChange = (event) => { + if (theme === 'system') { + setActualTheme(event.matches ? 'dark' : 'light') + } + } + + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') + + // Set initial theme + if (theme === 'system') { + setActualTheme(mediaQuery.matches ? 'dark' : 'light') + } else { + setActualTheme(theme) + } + + mediaQuery.addEventListener('change', handleSystemThemeChange) + + return () => mediaQuery.removeEventListener('change', handleSystemThemeChange) + }, [theme, setActualTheme]) + + const currentTheme = getCurrentTheme() === 'dark' ? darkTheme : lightTheme + + return ( + + + + {children} + + + ) +} + +export default ThemeProvider diff --git a/client/src/providers/index.js b/client/src/providers/index.js new file mode 100644 index 00000000..ca1390d4 --- /dev/null +++ b/client/src/providers/index.js @@ -0,0 +1 @@ +export { default as ThemeProvider } from './Theme' diff --git a/client/src/reducers/auth.js b/client/src/reducers/auth.js deleted file mode 100644 index 81942650..00000000 --- a/client/src/reducers/auth.js +++ /dev/null @@ -1,16 +0,0 @@ -import { googleLogout } from '@react-oauth/google' -import { AUTH, LOGOUT } from '../constants/actionTypes' - -export default (state = { authData: null },action) => { - switch (action.type) { - case AUTH: - localStorage.setItem('profile', JSON.stringify({ ...action?.data })) - return { ...state, authData: action?.data } - case LOGOUT: - googleLogout() - localStorage.clear() - return { ...state, authData: null } - default: - return state - } -} diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js deleted file mode 100644 index 0283f0ce..00000000 --- a/client/src/reducers/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import { combineReducers } from "redux" - -import posts from "./posts" -import auth from "./auth" - -export default combineReducers({ posts, auth }) diff --git a/client/src/reducers/posts.js b/client/src/reducers/posts.js deleted file mode 100644 index 50be4d2a..00000000 --- a/client/src/reducers/posts.js +++ /dev/null @@ -1,124 +0,0 @@ -import { - // - FETCH_RECOMMENDED, - FETCHING_RECOMMENDED_POSTS, - FETCHED_RECOMMENDED_POSTS, - FETCH_ALL, - FETCH_CREATED, - FETCHING_CREATED_POSTS, - FETCHED_CREATED_POSTS, - FETCHING_LIKED_POSTS, - FETCHED_LIKED_POSTS, - FETCH_LIKED, - CREATE, - UPDATE, - FETCH_POST, - DELETE, - DELETE_COMMENT, - FETCH_BY_SEARCH, - START_LOADING, - END_LOADING, - COMMENT, - USER_DETAILS, - FETCHING_PRIVATE_POSTS, - FETCHED_PRIVATE_POSTS, - FETCH_PRIVATE, - FETCHING_COMMENTS, - FETCH_COMMENTS, - FETCHED_COMMENTS, - CREATE_COMMENT, - CREATING_POST, - CREATED_POST, - DELETING_POST, - DELETED_POST, -} from '../constants/actionTypes' - -export default ( - - state = { - isFetchingCreatedPosts: true, - isFetchingLikedPosts: true, - isFetchingPrivatePosts: true, - isFetchingRecommendedPosts: true, - isFetchingComments: true, - isCreatingPost: false, - isDeletingPost: false, - isLoading: true, - posts: [], - data: {}, - likedPosts: [], - createdPosts: [], - recommendedPosts: [], - privatePosts: [], - comments: [], - userComments: [], - },action -) => { - switch (action.type) { - case FETCHING_CREATED_POSTS: - return { ...state, isFetchingCreatedPosts: true } - case FETCHING_LIKED_POSTS: - return { ...state, isFetchingLikedPosts: true } - case FETCHING_PRIVATE_POSTS: - return { ...state, isFetchingPrivatePosts: true } - case FETCHING_RECOMMENDED_POSTS: - return { ...state, isFetchingRecommendedPosts: true } - case FETCHING_COMMENTS: - return { ...state, isFetchingComments: true } - case START_LOADING: - return { ...state, isLoading: true } - case FETCH_COMMENTS: - return { ...state, comments: action.payload.comments ? [] : action.payload, commentsNumberOfPages: action.payload.numberOfPages, userComments: action.payload.comments } - case FETCH_ALL: - return { ...state, posts: action.payload.data, currentPage: action.payload.currentPage, numberOfPages: action.payload.numberOfPages } - case FETCH_BY_SEARCH: - return { ...state, posts: action.payload.data } - case USER_DETAILS: - return { ...state, data: action.payload.data } - case FETCH_LIKED: - return { ...state, likedPosts: action.payload.data, likedNumberOfPages: action.payload.numberOfPages } - case FETCH_CREATED: - return { ...state, createdPosts: action.payload.data, createdNumberOfPages: action.payload.numberOfPages } - case FETCH_PRIVATE: - return { ...state, privatePosts: action.payload.data, privateNumberOfPages: action.payload.numberOfPages } - case FETCH_RECOMMENDED: - return { ...state, recommendedPosts: action.payload.data } - case CREATE_COMMENT: - return { ...state, comments: [...state.comments, action.payload] } - case CREATE: - return { ...state, posts: [...state.posts, action.payload] } - case FETCH_POST: - return { ...state, post: action.payload.post } - case UPDATE: - return { ...state, posts: state.posts.map((post) => (post._id === action.payload._id ? action.payload : post)) } - case DELETE: - return { ...state, posts: state.posts.filter((post) => (post._id !== action.payload._id ? action.payload : post)) } - case DELETE_COMMENT: - return { ...state, comments: state.comments.filter((comment) => comment._id !== action.payload) } - case COMMENT: - return { ...state, posts: state.posts.map((post) => (post._id === Number(action.payload._id) ? action.payload : post)) } - case END_LOADING: - return { ...state, isLoading: false } - case FETCHED_RECOMMENDED_POSTS: - return { ...state, isFetchingRecommendedPosts: false } - case FETCHED_PRIVATE_POSTS: - return { ...state, isFetchingPrivatePosts: false } - case FETCHED_LIKED_POSTS: - return { ...state, isFetchingLikedPosts: false } - case FETCHED_CREATED_POSTS: - return { ...state, isFetchingCreatedPosts: false } - case FETCHED_COMMENTS: - return { ...state, isFetchingComments: false } - case CREATING_POST: - return { ...state, isCreatingPost: true } - case CREATED_POST: - return { ...state, isCreatingPost: false } - case DELETING_POST: - return { ...state, isDeletingPost: true } - case DELETED_POST: - return { ...state, isDeletingPost: false } - - default: - return state - } -} diff --git a/client/src/routes/Auth.jsx b/client/src/routes/Auth.jsx new file mode 100644 index 00000000..72a80a0d --- /dev/null +++ b/client/src/routes/Auth.jsx @@ -0,0 +1,13 @@ +import { useAuth } from '@clerk/clerk-react' +import { Navigate } from 'react-router-dom' + +const Auth = ({ component }) => { + const { isLoaded, isSignedIn } = useAuth() + + if (!isLoaded) { + return null + } + return isSignedIn ? : component +} + +export default Auth diff --git a/client/src/routes/Private.jsx b/client/src/routes/Private.jsx new file mode 100644 index 00000000..4ff7bc4b --- /dev/null +++ b/client/src/routes/Private.jsx @@ -0,0 +1,12 @@ +import { useAuth } from '@clerk/clerk-react' +import { Navigate } from 'react-router-dom' + +const Private = ({ component }) => { + const { isLoaded, isSignedIn } = useAuth() + if (!isLoaded) { + return null + } + return isSignedIn ? component : +} + +export default Private diff --git a/client/src/routes/index.js b/client/src/routes/index.js new file mode 100644 index 00000000..1a3dad41 --- /dev/null +++ b/client/src/routes/index.js @@ -0,0 +1,2 @@ +export { default as AuthRoute } from './Auth' +export { default as PrivateRoute } from './Private' diff --git a/client/src/sections/Comments.jsx b/client/src/sections/Comments.jsx new file mode 100644 index 00000000..e5ea3bba --- /dev/null +++ b/client/src/sections/Comments.jsx @@ -0,0 +1,106 @@ +import { useState } from 'react' +import { Avatar, Box, Button, ButtonGroup, Card, CardContent, CardHeader, Fade, IconButton, Stack, TextField, Typography } from '@mui/material' +import { ThumbUp, Send, ArrowDownward, Cancel } from '@mui/icons-material' +import { comments as dummyComments } from '@/data/comments' +import moment from 'moment' +import { useUser } from '@clerk/clerk-react' +import { UserAvatar } from '@/components' +import { Link, useNavigate } from 'react-router-dom' + +const CommentInput = ({ setComments, comments }) => { + // TODO: Fix later by add comment + const { user } = useUser() + const navigate = useNavigate() + const [comment, setComment] = useState('') + + const handleSubmit = (e) => { + e.preventDefault() + + const newComment = { + id: comments.length + 1, + author: user.fullName, + content: comment, + likes: 0, + avatar: user.imageUrl + } + setComments([newComment, ...comments]) + setComment('') + } + const handleChange = (e) => setComment(e.target.value) + + if (!user) { + return null + } + + return ( + + + navigate('/user')} /> + + + + + + + + + + + ) +} + +const Comments = () => { + const [comments, setComments] = useState(dummyComments) + const [visibleComments, setVisibleComments] = useState(5) + + const handleLike = (id) => { + setComments(comments.map((comment) => (comment.id === id ? { ...comment, likes: comment.likes + 1 } : comment))) + } + + return ( + + + + + {comments.slice(0, visibleComments).map((comment) => ( + + + + + + {comment.author} + + + {moment(comment.createdAt).fromNow()} + + + + {comment.content} + + + handleLike(comment.id)}> + + + + {comment.likes} {comment.likes === 1 ? 'like' : 'likes'} + + + + + ))} + + {visibleComments < comments.length && ( + + )} + + + ) +} + +export default Comments diff --git a/client/src/sections/Recommendation.jsx b/client/src/sections/Recommendation.jsx new file mode 100644 index 00000000..0175ac37 --- /dev/null +++ b/client/src/sections/Recommendation.jsx @@ -0,0 +1,11 @@ +import { Card, CardHeader } from '@mui/material' + +const Recommendation = () => { + return ( + + + + ) +} + +export default Recommendation diff --git a/client/src/sections/index.js b/client/src/sections/index.js new file mode 100644 index 00000000..2a992147 --- /dev/null +++ b/client/src/sections/index.js @@ -0,0 +1,2 @@ +export { default as CommentSection } from './Comments.jsx' +export { default as RecommendationSection } from './Recommendation.jsx' diff --git a/client/src/service-worker.js b/client/src/service-worker.js deleted file mode 100644 index 7129fddd..00000000 --- a/client/src/service-worker.js +++ /dev/null @@ -1,63 +0,0 @@ -/* eslint-disable no-restricted-globals */ - -// This service worker can be customized! -// See https://developers.google.com/web/tools/workbox/modules -// for the list of available Workbox modules, or add any other -// code you'd like. -// You can also remove this file if you'd prefer not to use a -// service worker, and the Workbox build step will be skipped. - -import { clientsClaim } from 'workbox-core' -import { ExpirationPlugin } from 'workbox-expiration' -import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching' -import { registerRoute } from 'workbox-routing' -import { StaleWhileRevalidate } from 'workbox-strategies' - -clientsClaim() - -// Precache all of the assets generated by your build process. -// Their URLs are injected into the manifest variable below. -// This variable must be present somewhere in your service worker file, -// even if you decide not to use precaching. See https://cra.link/PWA -precacheAndRoute(self.__WB_MANIFEST) - -// Set up App Shell-style routing, so that all navigation requests -// are fulfilled with your index.html shell. Learn more at -// https://developers.google.com/web/fundamentals/architecture/app-shell -const fileExtensionRegexp = /[^/?]+\.[^/]+$/ -registerRoute( - // Return false to exempt requests from being fulfilled by index.html. - ({ request, url }) => { - if (request.mode !== 'navigate' || url.pathname.startsWith('/_') || !fileExtensionRegexp.test(url.pathname)) { - return false; - } else { - return true; - } - }, - createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html') -) - -// An example runtime caching route for requests that aren't handled by the -// precache, in this case same-origin .png requests like those from in public/ -registerRoute( - // Add in any other file extensions or routing criteria as needed. - ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst. - new StaleWhileRevalidate({ - cacheName: 'images', - plugins: [ - // Ensure that once this runtime cache reaches a maximum size the - // least-recently used images are removed. - new ExpirationPlugin({ maxEntries: 50 }), - ], - }) -) - -// This allows the web app to trigger skipWaiting via -// registration.waiting.postMessage({type: 'SKIP_WAITING'}) -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting() - } -}) - -// Any other custom service worker logic can go here. diff --git a/client/src/serviceWorkerRegistration.js b/client/src/serviceWorkerRegistration.js deleted file mode 100644 index bd07c7b4..00000000 --- a/client/src/serviceWorkerRegistration.js +++ /dev/null @@ -1,116 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://cra.link/PWA - -const isLocalhost = Boolean(window.location.hostname === 'localhost' || window.location.hostname === '[::1]' || window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)) - -const registerValidSW = (swUrl, config) => { - navigator.serviceWorker - .register(swUrl) - .then((registration) => { - registration.onupdatefound = () => { - const installingWorker = registration.installing - if (installingWorker === null) { - return - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log('New content is available and will be used when all tabs for this page are closed. See https://cra.link/PWA.') - - // Execute callback - if (config?.onUpdate) { - config.onUpdate(registration) - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.') - - // Execute callback - if (config?.onSuccess) { - config.onSuccess(registration) - } - } - } - } - } - }) - .catch((error) => { - console.error('Error during service worker registration:', error) - }) -} - -// Check if the service worker can be found. If it can't reload the page. -const checkValidServiceWorker = (swUrl, config) => { - fetch(swUrl, { headers: { 'Service-Worker': 'script' } }) - .then((response) => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type') - if (response.status === 404 || (contentType !== null && contentType.indexOf('javascript') === -1)) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then((registration) => { - registration.unregister().then(() => { - window.location.reload() - }) - }) - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config) - } - }) - .catch(() => { - console.log('No internet connection found. App is running in offline mode.') - }) -} - -export const register = (config) => { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config) - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log('This web app is being served cache-first by a service ' + 'worker. To learn more, visit https://cra.link/PWA') - }) - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config) - } - }) - } -} - - - -export const unregister = () => { - if ('serviceWorker' in navigator) - navigator.serviceWorker.ready // - .then((registration) => registration.unregister()) - .catch((error) => console.error(error.message)) -} diff --git a/client/src/store/index.js b/client/src/store/index.js new file mode 100644 index 00000000..b45e131d --- /dev/null +++ b/client/src/store/index.js @@ -0,0 +1,58 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export const useStore = create( + persist( + (set, get) => ({ + // Posts state and actions + posts: [], + setPosts: (posts) => set({ posts }), + addPost: (post) => set((state) => ({ posts: [post, ...state.posts] })), + updatePost: (updatedPost) => + set((state) => ({ + posts: state.posts.map((post) => (post.id === updatedPost.id ? updatedPost : post)) + })), + removePost: (id) => + set((state) => ({ + posts: state.posts.filter((post) => post.id !== id) + })), + + // Theme state and actions + theme: 'system', + setTheme: (theme) => set({ theme }), + actualTheme: 'dark', + setActualTheme: (actualTheme) => set({ actualTheme }), + getCurrentTheme: () => { + const state = get() + return state.theme === 'system' ? state.actualTheme : state.theme + }, + setThemeAndActual: (theme) => { + set({ theme }) + if (theme !== 'system') { + set({ actualTheme: theme }) + } + }, + + // Snackbar state and actions + snackbar: { + open: false, + message: '', + severity: 'info' + }, + openSnackbar: (message, severity = 'info') => set({ snackbar: { open: true, message, severity } }), + closeSnackbar: () => set((state) => ({ snackbar: { ...state.snackbar, open: false } })), + + // Pages state and actions + pages: [], + setPages: (pages) => set({ pages }) + }), + { + name: 'app-storage', + // Only persist theme-related state + partialize: (state) => ({ + theme: state.theme, + actualTheme: state.actualTheme + }) + } + ) +) diff --git a/client/src/styles/index.css b/client/src/styles/index.css new file mode 100644 index 00000000..6ddf64df --- /dev/null +++ b/client/src/styles/index.css @@ -0,0 +1,81 @@ +@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); + +body { + background-color: #0f1214; + margin: 0; + color-scheme: light dark; /* Enable system color scheme support */ +} + +* { + font-family: Heebo, sans-serif !important; +} + +/* Modern scrollbar styling */ +:root { + --scrollbar-width: 8px; + --scrollbar-height: 8px; + --scrollbar-track-color: transparent; + --scrollbar-thumb-color-light: rgba(0, 0, 0, 0.2); + --scrollbar-thumb-color-dark: rgba(255, 255, 255, 0.2); + --scrollbar-thumb-hover-color-light: rgba(0, 0, 0, 0.3); + --scrollbar-thumb-hover-color-dark: rgba(255, 255, 255, 0.3); +} + +/* Webkit scrollbar */ +::-webkit-scrollbar { + width: var(--scrollbar-width); + height: var(--scrollbar-height); +} + +::-webkit-scrollbar-track { + background: var(--scrollbar-track-color); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-color-light); + border-radius: 10px; + border: 2px solid transparent; + background-clip: padding-box; + transition: background-color 0.2s ease-in-out; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover-color-light); +} + +/* Firefox scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color-light) var(--scrollbar-track-color); +} + +/* Dark mode adjustments */ +@media (prefers-color-scheme: dark) { + body { + color: #ffffff; + } + + ::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb-color-dark); + } + + ::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover-color-dark); + } + + * { + scrollbar-color: var(--scrollbar-thumb-color-dark) var(--scrollbar-track-color); + } +} + +/* Hide scrollbar while not scrolling (optional) */ +@media (hover: hover) { + ::-webkit-scrollbar-thumb { + opacity: 0; + } + + :hover::-webkit-scrollbar-thumb { + opacity: 1; + } +} \ No newline at end of file diff --git a/client/src/themes/index.js b/client/src/themes/index.js new file mode 100644 index 00000000..d9348771 --- /dev/null +++ b/client/src/themes/index.js @@ -0,0 +1,48 @@ +import { createTheme } from '@mui/material' + +const Light = createTheme({ + breakpoints: {}, + palette: { + mode: 'light', + primary: { + main: '#3f51b5' + }, + secondary: { + main: '#f50057' + }, + text: { + secondary: { + main: '#000000', + muted: '#808080' + } + }, + background: { + paper: '#ffffff', + default: '#f5f5f5' + } + } +}) + +const Dark = createTheme({ + palette: { + mode: 'dark', + primary: { + main: '#5f57ff' + }, + secondary: { + main: '#ffffff' + }, + text: { + secondary: { + main: '#ffffff', + muted: '#808080' + } + }, + background: { + paper: '#000000', + default: '#0d1017' + } + } +}) + +export { Light, Dark } diff --git a/client/vercel.json b/client/vercel.json index 9bf2b05b..389e87cc 100644 --- a/client/vercel.json +++ b/client/vercel.json @@ -1,9 +1,9 @@ { - "routes": [ - { - "src": "/[^.]+", - "dest": "/", - "status": 200 - } - ] -} + "routes": [ + { + "src": "/[^.]+", + "dest": "/", + "status": 200 + } + ] +} \ No newline at end of file diff --git a/client/vite.config.js b/client/vite.config.js index 094b203e..be8bf127 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -1,7 +1,17 @@ +import path from 'path' +import react from '@vitejs/plugin-react-swc' import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + '~': path.resolve(__dirname, 'node_modules') + } + }, + build: { + chunkSizeWarningLimit: 1600 + } }) diff --git a/rules/ENV_VARIABLES.md b/rules/ENV_VARIABLES.md deleted file mode 100644 index d80698fe..00000000 --- a/rules/ENV_VARIABLES.md +++ /dev/null @@ -1,68 +0,0 @@ -# ENVIRONMENT VARIABLES - -- This file contains setup rules for the environment variables of this Project. -- All the environemnt variable are stored in `.env.local` file, which is not tracked by Git, **you need to manually create this file**. -- The keys of those variables are stored in `.env.example` file, for referencing purpose. -- For working with the environment variables, you have to create your own by following the given steps. -- ⚠️ Never keep any environment variables inside `.env.example` since it's tracked by Git and the history stays forever in VCS, instead always use `env.local`. - -## Table of Contents - -- **[CLIENT:](#client)** - - [VITE_GOOGLE_CLIENT_ID](#vite_google_client_id) -- **[SERVER:](#server)** - - [CONNECTION_URL](#connection_url) - - [USER](#user) - - [PASS](#pass) - - [BASE_URL](#base_url) - - [TOKEN_SECRET](#token_secret) - -## CLIENT - -All the references to the environment variables of Client is stored in _[client/.env.example](../client/.env.example)_ - -- ### VITE_GOOGLE_CLIENT_ID - - - This variable is used for Google SignIn authentication. - - Log into [Google Cloud Platform](https://console.cloud.google.com) - - Create a **new Project** By Clicking on the dropbox in top Left corner - - Click Create Credentials. - - Select OAuth Client ID. - - Configure Consent Screen. - - Select External Users. - - Continue the rest and create OAuth Credentials. - - Use the OAuth Client ID genrerated for this variable. - -## SERVER - -All the references to the environment variables of Server is stored in _[server/.env.example](../server/.env.example)_ - -- ### CONNECTION_URL - - - Go to [Mongo DB Atlas](https://www.mongodb.com/cloud) and create an account for free. - - Create a new Project. - - Build a new Database using M0 configuration as it is FREE. - - Add Username, password, Current IP address and finish creating the database. - - Click on Connect and Select Drivers option. - - Copy the Connection URL, it might look like this: - `mongodb+srv://username:@cluster0.abcdefg.mongodb.net/?retryWrites=true&w=majority` - - Paste it in .env.local corresponding to the variable name - -- ### USER - - - This Goolge email account will be used to send reset password links. - - You can use your own email or create a new Email ID for this specific puropse. - -- ### PASS - - - Read this [article](https://community.nodemailer.com/using-gmail) to generate an app specific password for the above email account. - -- ### BASE_URL - - - This variable is used to determine the domain of the client for appending it in the beginnning of the passwoed reset link. - - The 2 possible options are already given in `.env.example` - -- ### TOKEN_SECRET - - - Used to encode the JWT token for authenticaltion - - Any random string can be a TOKEN_SECRET diff --git a/rules/SETUP.md b/rules/SETUP.md deleted file mode 100644 index 71d07d08..00000000 --- a/rules/SETUP.md +++ /dev/null @@ -1,50 +0,0 @@ -# Memories setup guidelines πŸš€ - -**Hello contributor, I want you to stick to the below listed Setup guidelines to successfully setup the react app in your local system and get started with developing!!** - -
- -- Check if your branch is behind the original branch. -- Always update the branch with original branch `memories:main` before starting any new developement. -- `Fork` the repo to your account. -- Open Git bash in a folder. -- Enter this command to clone the repo: - `git clone https://github.com/[your-github-id]/memories.git` -- Now you have the repo in your **local storage** -- Open the project in VSCode or any Code Editor of your choice. -- Go inside `client` folder by using `cd client`. -- Next to setup the react app `npm i` in the terminal inside your project directory. -- Set Up Environment Variables, take reference from **[ENV_VARIABLES](./ENV_VARIABLES.md)** file. -- Once the node modules and other stuffs are installed , `npm run dev` to start the app in `http://localhost:3000/` -- You can acess the `server` folder to get the backend and start it with `npm start` in `http://localhost:5000/` - - Currently since the server is already deployed, hence it uses the deployed server by default. - - If you make changes in server files, then navigate to `client/src/api/` - - Open `index.jsx` file. - - Change the index of the array from `const API = axios.create({ baseURL: apiURL[0] })` to `...apiURL[1]...` - - This will make the app use the locally running server -- Make changes **according** to the Tasks assigned to you -- Maintain the folder structure , keep small components like **Navbar, Home** in `src\components` folder -- We have used **Material UI & Styled Components** for styling, Styling files are available in `[component]\styles.js`. -- You are also allowed to use GOOGLE FONTS for same fonts as of figma files. -- Other extra **Pictures, icons , svgs** are to be kept in `src\images\` -- Once you are done with the changes , `cd ..` to come to the root folder -- Once all server related changes are done locally - - Reset the index of the array from `const API = axios.create({ baseURL: apiURL[1] })` to `...apiURL[0]...` to reuse the deployed server -- `git pull` to pull the latest version of the code -- `git add .` to stage for commits -- `git commit -am "message"` for commiting the code. -- **REMEMBER** --> YOU NEED TO PULL REQ ON `main` BRANCH !! -- Once done create a Pull Request and wait for the repo-owner to review. -- Attach proper **`Screenshots, Proper Description and Issue Number` in the Pull request** -- Dont forget to ⭐ the repository on github -
- -## For moderators - -- Before merging a PR with big changes. -- `git branch -b ` -- `git fetch origin refs/pull/#PR_number/head:` -- `git checkout ` -- Do testing with `npm start` -- Approve or Review changes in PR accordingly -- References [Stack Overflow Link](https://stackoverflow.com/questions/14947789/github-clone-from-pull-request) diff --git a/server/.env.example b/server/.env.example index 649bd3a9..4853cc49 100644 --- a/server/.env.example +++ b/server/.env.example @@ -1,5 +1,6 @@ -CONNECTION_URL = "MONGODB URL STRING" -USER = "sender's email address" -PASS = "sender's password" -BASE_URL = https://memories-pritam.vercel.app or http://localhost:3000 -TOKEN_SECRET = +CLERK_WEBHOOK_SECRET= +CLERK_JWT_TOKEN= +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= diff --git a/server/.gitignore b/server/.gitignore index cb0f41fa..f27b63d9 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -1,3 +1,43 @@ -node_modules -playground.mongodb -.env +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun +.vercel diff --git a/server/ERD.mmd b/server/ERD.mmd new file mode 100644 index 00000000..3339b945 --- /dev/null +++ b/server/ERD.mmd @@ -0,0 +1,62 @@ +erDiagram + USERS { + uuid id PK + varchar first_name + varchar last_name + varchar email UK + varchar password_hash + text bio + varchar profile_image_url + timestamp created_at + timestamp updated_at + } + POSTS { + uuid id PK + uuid author_id FK + varchar title + text description + varchar image_url + enum visibility + integer reaction_count + timestamp created_at + timestamp updated_at + } + TAGS { + uuid id PK + varchar name UK + } + COMMENTS { + uuid id PK + uuid post_id FK + uuid author_id FK + text content + integer like_count + timestamp created_at + timestamp updated_at + } + POST_TAGS { + uuid post_id FK + uuid tag_id FK + } + POST_REACTIONS { + uuid id PK + uuid post_id FK + uuid user_id FK + enum reaction_type + timestamp created_at + } + COMMENT_LIKES { + uuid comment_id FK + uuid user_id FK + timestamp created_at + } + + USERS ||--o{ POSTS : creates + USERS ||--o{ COMMENTS : writes + POSTS ||--o{ COMMENTS : has + POSTS ||--o{ POST_TAGS : has + TAGS ||--o{ POST_TAGS : belongs_to + USERS ||--o{ POST_REACTIONS : makes + POSTS ||--o{ POST_REACTIONS : receives + USERS ||--o{ COMMENT_LIKES : gives + COMMENTS ||--o{ COMMENT_LIKES : receives \ No newline at end of file diff --git a/server/README.md b/server/README.md new file mode 100644 index 00000000..32e5fb08 --- /dev/null +++ b/server/README.md @@ -0,0 +1,89 @@ +# Elysia with Bun runtime + +## Getting Started +To get started with this template, simply paste this command into your terminal: +```bash +bun create elysia ./elysia-example +``` + +## Development +To start the development server run: +```bash +bun run dev +``` + +Open http://localhost:5000/ with your browser to see the result. + +### Prisma +To start the Prisma studio run: +```bash +bun run prisma studio +``` + +### ERD + +```mermaid +erDiagram + USERS { + uuid id PK + varchar first_name + varchar last_name + varchar email UK + varchar password_hash + text bio + varchar profile_image_url + timestamp created_at + timestamp updated_at + } + POSTS { + uuid id PK + uuid author_id FK + varchar title + text description + varchar image_url + enum visibility + integer reaction_count + timestamp created_at + timestamp updated_at + } + TAGS { + uuid id PK + varchar name UK + } + COMMENTS { + uuid id PK + uuid post_id FK + uuid author_id FK + text content + integer like_count + timestamp created_at + timestamp updated_at + } + POST_TAGS { + uuid post_id FK + uuid tag_id FK + } + POST_REACTIONS { + uuid id PK + uuid post_id FK + uuid user_id FK + enum reaction_type + timestamp created_at + } + COMMENT_LIKES { + uuid comment_id FK + uuid user_id FK + timestamp created_at + } + + USERS ||--o{ POSTS : creates + USERS ||--o{ COMMENTS : writes + POSTS ||--o{ COMMENTS : has + POSTS ||--o{ POST_TAGS : has + TAGS ||--o{ POST_TAGS : belongs_to + USERS ||--o{ POST_REACTIONS : makes + POSTS ||--o{ POST_REACTIONS : receives + USERS ||--o{ COMMENT_LIKES : gives + COMMENTS ||--o{ COMMENT_LIKES : receives + +``` \ No newline at end of file diff --git a/server/bun.lockb b/server/bun.lockb new file mode 100644 index 00000000..8b9da755 Binary files /dev/null and b/server/bun.lockb differ diff --git a/server/controllers/comments.js b/server/controllers/comments.js deleted file mode 100644 index e953e888..00000000 --- a/server/controllers/comments.js +++ /dev/null @@ -1,60 +0,0 @@ -import express from 'express' -import mongoose from 'mongoose' -import Comment from '../models/comment.js' -import { setCreator } from './posts.js' -import { getUser } from './user.js' - -const router = express.Router() - -export const createComment = async (req, res) => { - const comment = req.body - try { - const { creator: userId, post: postId, message } = comment - const newComment = new Comment({ - post: new mongoose.Types.ObjectId(postId), - creator: new mongoose.Types.ObjectId(userId), - message, - }) - await newComment.save() - const creator = await getUser(userId) - const result = { ...newComment._doc, creator } - res.status(201).json(result) - } catch (error) { - res.status(409).json({ message: error.message }) - } -} - -export const getComments = async (req, res) => { - try { - const { id } = req.params - const postId = new mongoose.Types.ObjectId(id) - let comments = await Comment.aggregate([ - { $match: { post: postId } }, // - { $sort: { createdAt: -1 } }, - { - $lookup: { - from: 'users', - localField: 'creator', - foreignField: '_id', - as: 'creator', - }, - }, - ]) - comments = setCreator(comments) - res.status(200).json(comments) - } catch (error) { - res.status(409).json({ message: error.message }) - } -} - -export default router - -export const deleteComment = async (req, res) => { - try { - const { id } = req.params - await Comment.findByIdAndDelete(id) - res.json({ message: 'Comment deleted Successfully' }) - } catch (error) { - res.json({ message: error.message }) - } -} diff --git a/server/controllers/posts.js b/server/controllers/posts.js deleted file mode 100644 index 7e085e34..00000000 --- a/server/controllers/posts.js +++ /dev/null @@ -1,202 +0,0 @@ -import express from 'express' -import mongoose from 'mongoose' -import Post from '../models/post.js' -import Media from '../models/media.js' -import Comment from '../models/comment.js' - -const router = express.Router() - -export const setCreator = (posts) => - posts.map((post) => { - delete post.creator[0].password - delete post.creator[0].resetToken - return { ...post, creator: post.creator[0] } - }) - -export const getPosts = async (req, res) => { - const { page } = req.query - - try { - const userId = new mongoose.Types.ObjectId(req.userId) - const query = { $or: [{ private: false }, { creator: userId }] } - const LIMIT = 8 - const total = await Post.countDocuments(query) - const startIndex = (Number(page) - 1) * LIMIT // get the starting index of every page - console.log('Fetching posts') - const start = Date.now() - let posts = await Post.aggregate([ - { $match: query }, - { $sort: { createdAt: -1 } }, - { $skip: startIndex }, - { $limit: LIMIT }, - { - $lookup: { - from: 'users', - localField: 'creator', - foreignField: '_id', - as: 'creator', - }, - }, - ]) - posts = setCreator(posts) - const end = Date.now() - console.log(`Fetching took ${(end - start) / 1000} seconds`) - res.status(200).json({ data: posts, currentPage: Number(page), numberOfPages: Math.ceil(total / LIMIT) }) - } catch (error) { - console.log(error) - res.status(404).json({ message: error.message }) - } -} - -export const getPost = async (req, res) => { - const { id } = req.params - try { - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(404).send('Invalid Id. Post Not Found') - } - let posts = await Post.aggregate([ - { $match: { _id: new mongoose.Types.ObjectId(id) } }, - { - $lookup: { - from: 'users', - localField: 'creator', - foreignField: '_id', - as: 'creator', - }, - }, - { - $lookup: { - from: 'media', - localField: '_id', - foreignField: '_id', - as: 'media', - }, - }, - { - $addFields: { - image: { $arrayElemAt: ['$media.image', 0] }, - }, - }, - { - $project: { - thumbnail: 0, - media: 0, - }, - }, - ]) - - posts = setCreator(posts) - if (!posts.length) { - return res.status(404).send('Post not found with that id') - } - const post = posts[0] - if (post.private && `${post.creator._id}` !== req.userId) { - return res.status(404).send("Can't access post with that Id") - } - res.status(200).json(post) - } catch (error) { - console.log(error) - res.status(404).json({ message: error.message }) - } -} - -export const getPostsBySearch = async (req, res) => { - const { searchQuery, tags } = req.query - - try { - const title = new RegExp(searchQuery, 'i') - const userId = new mongoose.Types.ObjectId(req.userId) - const query = { - $and: [ - { - $or: [{ creator: userId }, { private: false }], - }, - { - $and: [ - { title }, // Include title condition if not empty - tags ? { tags: { $in: tags.split(',') } } : {}, // Include tags condition if tags are given - ], - }, - ], - } - - let posts = await Post.aggregate([ - { $match: query }, - { $sort: { createdAt: -1 } }, - { - $lookup: { - from: 'users', - localField: 'creator', - foreignField: '_id', - as: 'creator', - }, - }, - ]) - posts = setCreator(posts) - - res.status(200).json({ data: posts }) - } catch (error) { - res.status(404).json({ message: error.message }) - } -} - -export const createPost = async (req, res) => { - const post = req.body - const media = post.image - try { - const newPost = new Post({ - ...post, - creator: new mongoose.Types.ObjectId(post.creator), - }) - await newPost.save() - - const newMedia = new Media({ - _id: newPost._id, - image: media, - }) - await newMedia.save() - res.status(201).json(newPost) - } catch (error) { - res.status(409).send(error.message) - } -} - -export const updatePost = async (req, res) => { - const { id: _id } = req.params - if (!mongoose.Types.ObjectId.isValid(_id)) return res.status(404).send('No post with that id') - - const post = req.body - const updatedPost = await Post.findByIdAndUpdate(_id, { ...post, _id }, { new: true }) - await Media.findByIdAndUpdate(_id, { image: post.image }) - res.status(200).json(updatedPost) -} - -export const deletePost = async (req, res) => { - const { id } = req.params - if (!mongoose.Types.ObjectId.isValid(id)) { - return res.status(404).send('No post with that id') - } - await Post.findByIdAndRemove(id) - await Media.findByIdAndRemove(id) - await Comment.deleteMany({ post: new mongoose.Types.ObjectId(id) }) - res.json({ message: 'Post deleted Successfully' }) -} - -export const getAllTags = async (req, res) => { - try { - // Use the aggregation framework to retrieve all unique tags from posts - const tags = await Post.aggregate([ - { $unwind: '$tags' }, // Unwind the 'tags' array - { $group: { _id: '$tags' } }, // Group by tags - ]); - - // Extract the tag names from the aggregation result - const tagNames = tags.map(tag => tag._id); - - res.status(200).json({data:tagNames}); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal server error' }); - } - } -export default router diff --git a/server/controllers/user.js b/server/controllers/user.js deleted file mode 100644 index 787949ee..00000000 --- a/server/controllers/user.js +++ /dev/null @@ -1,336 +0,0 @@ -import bcrypt from 'bcryptjs' -import jwt from 'jsonwebtoken' -import lodash from 'lodash' -import User from '../models/user.js' -import Post from '../models/post.js' -import Comment from '../models/comment.js' -import crypto from 'crypto' -import { sendEmail } from '../utils/emailSender.js' -import mongoose from 'mongoose' -import dotenv from 'dotenv' - -dotenv.config() - -const secret = process.env.TOKEN_SECRET - -const regex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ -const countOccurrences = (arr, val) => arr.reduce((a, v) => (v === val ? a + 1 : a), 0) -const getTop5Tags = ({ allTags: tags }) => { - let frequency = {} - tags.forEach((tag) => (frequency[tag] = countOccurrences(tags, tag))) - tags.sort((self, other) => { - let diff = frequency[other] - frequency[self] - if (diff === 0) diff = frequency[other] - frequency[self] - return diff - }) - return [...new Set(tags)].splice(0, 5) -} -export const signin = async (req, res) => { - const { email, password, remember } = req.body - try { - const existingUser = await User.findOne({ email }) - - if (!existingUser) { - return res.status(404).json({ message: "Invalid credentials" }) - } - const isPasswordCorrect = await bcrypt.compare(password, existingUser.password) - - if (!isPasswordCorrect) { - return res.status(401).json({ message: 'Invalid credentials' }) - } - - const token = jwt.sign({ email: existingUser.email, id: existingUser._id }, secret, remember ? null : { expiresIn: '1h' }) - res.status(200).json({ result: existingUser, token }) - } catch (error) { - console.log(error) - res.status(500).json({ message: 'Something went wrong' }) - } -} -export const googleSignin = async (req, res) => { - const { name, email, image, googleId } = req.body - - try { - if (![name, email, image].every((field) => typeof field === "string")) { - return res.status(400).json({ status: "error" }); - } - const user = await User.findByIdAndUpdate(googleId, { name, email, image }, { upsert: true }) - res.status(200).json({ result: user }) - } catch (error) { - console.log(error.message) - res.status(500).json({ message: error.message }) - } -} -export const signup = async (req, res) => { - const { email, password, confirmPassword, firstName, lastName, avatar } = req.body - - try { - const users = await User.find({ email }) - let existingUser = false - users.forEach((user) => { - if (user.password) existingUser = true - }) - - if (existingUser) { - return res.status(409).json({ message: 'User already exists' }) - } - if (!regex.test(email)) { - return res.status(501).json({ message: 'Invalid Email ID' }) - } - if (password !== confirmPassword) { - return res.status(409).json({ message: "Passwords don't match" }) - } - if (password.length < 6) { - return res.status(409).json({ message: 'Password length must be greater than 6 characters' }) - } - - const hashedPassword = await bcrypt.hash(password, 12) - const result = await User.create({ email, password: hashedPassword, name: `${firstName} ${lastName}`, avatar: avatar }) - const token = jwt.sign({ email: result.email, id: result._id }, secret, { expiresIn: '1h' }) - - res.status(201).json({ result, token }) - } catch (error) { - res.status(500).json({ message: 'Something went wrong' }) - } -} -export const updateDetails = async (req, res) => { - const { firstName, lastName, avatar, email, id, prevPassword, newPassword } = req.body - - try { - const { name, email: oldEmail, avatar: oldAvatar, password } = await User.findById(id) - // Check for same existing data posted - const newPasswordsSame = await bcrypt.compare(newPassword ?? '', password) - const oldPasswordsDifferent = !(await bcrypt.compare(prevPassword, password)) - const sameData = - name.split(' ')[0] === firstName && // - name.split(' ')[1] === lastName && - oldEmail === email && - (!newPassword || newPasswordsSame) && - lodash.isEqual(oldAvatar, avatar) - if (sameData) { - return res.status(409).json({ message: 'No new updates were applied' }) - } - if (!regex.test(email)) { - return res.status(501).json({ message: 'Invalid Email ID' }) - } - if (oldPasswordsDifferent) { - return res.status(409).json({ message: 'Incorrect Previous Password' }) - } - if (newPassword && newPassword.length < 6) { - return res.status(409).json({ message: 'Password length must be greater than 6 characters' }) - } - const user = { - name: `${firstName} ${lastName}`, - email: email, - password: newPassword ? await bcrypt.hash(newPassword, 12) : password, - avatar: avatar, - } - await User.findByIdAndUpdate(id, { ...user, id }, { new: true }) - res.status(204).json({ message: 'Details Updated Successfully' }) - } catch (error) { - res.status(500).json({ message: 'Something went wrong.' }) - } -} -export const getUserDetails = async (req, res) => { - const { id } = req.params - - try { - const userId = new mongoose.Types.ObjectId(id) - const allTags = await Post.aggregate([ - { $match: { creator: userId } }, - { - $group: { - _id: null, - tags: { $push: '$tags' }, - }, - }, - { - $project: { - _id: 0, - allTags: { - $reduce: { - input: '$tags', - initialValue: [], - in: { - $concatArrays: ['$$this', '$$value'], - }, - }, - }, - }, - }, - ]) - const longestPost = ( - await Post.aggregate([ - { $match: { creator: userId } }, - { - $project: { - message: 1, - messageLength: { $strLenCP: '$message' }, - }, - }, - { $sort: { messageLength: -1 } }, - ]) - )[0] - const { email, image, avatar, name } = await User.findById(userId) - const result = { - name, - email, - image, - avatar, - postsCreated: await Post.countDocuments({ creator: userId }), - postsLiked: await Post.countDocuments({ likes: { $all: [userId] } }), - privatePosts: await Post.countDocuments({ - $and: [{ creator: userId }, { private: true }], - }), - totalLikesRecieved: - ( - await Post.aggregate([ - { $match: { creator: userId } }, - { - $group: { - _id: '_id', - totalValue: { - $sum: { - $size: '$likes', - }, - }, - }, - }, - ]) - )[0]?.totalValue || 0, - longestPostWords: longestPost?.message.split(' ').length || 0, - top5Tags: allTags.length ? getTop5Tags(allTags[0]) : allTags, - longestPostId: longestPost?._id, - } - res.status(200).json(result) - } catch (error) { - res.status(500).json({ message: error.message }) - } -} -export const getComments = async (req, res) => { - const { id } = req.params - const { page } = req.query - try { - const userId = new mongoose.Types.ObjectId(id) - const query = { creator: userId } - const LIMIT = 8 - const total = await Comment.countDocuments(query) - const startIndex = (Number(page) - 1) * LIMIT - - const comments = await Comment.aggregate([ - { $match: query }, - { $sort: { createdAt: -1 } }, - { $skip: startIndex }, - { $limit: LIMIT }, - { - $lookup: { - from: 'posts', - localField: 'post', - foreignField: '_id', - as: 'post', - }, - }, - ]) - comments.forEach(comment => { comment.post = comment.post[0] }) - res.status(200).json({ data: comments, numberOfPages: Math.ceil(total / LIMIT) }) - } catch (error) { - res.status(500).json({ message: error.message }) - } -} -export const getUserPostsByType = async (req, res) => { - const { id } = req.params - const { page, type } = req.query - const currentUser = req.userId - try { - if (type === 'private' && currentUser !== id) { - return req.status(404).send("Can't access private posts of other users") - } - const userId = new mongoose.Types.ObjectId(id) - const query = { - created: { creator: userId }, - liked: { likes: { $all: [userId] } }, - private: { $and: [{ creator: userId }, { private: true }] }, - } - - if (currentUser !== id) { - if (type === 'private') { - return req.status(404).send("Can't access private posts of other users") - } - query.created = { $and: [{ creator: userId }, { private: false }] } - query.liked = { $and: [{ likes: { $all: [userId] } }, { private: false }] } - } - - const LIMIT = 10 - const total = await Post.countDocuments(query[type]) - const startIndex = (Number(page) - 1) * LIMIT - const posts = await Post.find(query[type]).limit(LIMIT).sort({ createdAt: -1 }).skip(startIndex) - - res.status(200).json({ data: posts, numberOfPages: Math.ceil(total / LIMIT) }) - } catch (error) { - res.status(500).json({ message: error.message }) - } -} - -export const forgotPassword = async (req, res) => { - const { email } = req.body - try { - const existingUser = await User.findOne({ email }) - if (!existingUser) { - return res.status(404).json({ message: "User doesn't exist in dataBase" }) - } - if (existingUser.resetToken) { - return res.status(409).json({ message: 'Reset Link already sent. Please check your email' }) - } - - const { id } = existingUser - const token = crypto.randomBytes(32).toString('hex') - const URL = `${process.env.BASE_URL}/auth/forgotPassword/${id}/${token}` - existingUser.resetToken = token - await User.findByIdAndUpdate(id, { ...existingUser, id }, { new: true }) - - sendEmail(email, URL, res) - } catch (error) { - res.status(500).json({ message: error.message }) - } -} - -export const resetPassword = async (req, res) => { - const { id, token, newPassword } = req.body - try { - const existingUser = await User.findById(id) - - if (!existingUser || !token || token !== existingUser.resetToken) { - return res.status(404).json({ message: 'Invalid URL. Please try again' }) - } - if (newPassword.length < 6) { - return res.status(409).json({ message: 'Password length must be greater than 6 characters' }) - } - const passwordsSame = await bcrypt.compare(newPassword, existingUser.password) - if (passwordsSame) { - return res.status(406).json({ message: "New password can't be same as old password" }) - } - - const hashedPassword = await bcrypt.hash(newPassword, 12) - - existingUser.password = hashedPassword - existingUser.resetToken = null - - await User.findByIdAndUpdate(id, { ...existingUser, id }, { new: true }) - res.status(200).json() - } catch (error) { - res.status(500).json({ message: error.message }) - } -} - -export const getUser = async (id, res) => { - try { - const userId = id; - if (!userId === typeof "string") { - return res.status(404).json({ message: "Error." }); - } - const user = await User.findById(userId) - delete user.password - return user - } catch (error) { - console.log(error) - } -} diff --git a/server/index.js b/server/index.js deleted file mode 100644 index d30dafd0..00000000 --- a/server/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import { CommentRoutes, PostRoutes, UserRoutes } from './routes/index.js' -import express from 'express' -import bodyParser from 'body-parser' -import mongoose from 'mongoose' -import cors from 'cors' -import dotenv from 'dotenv' -// import { rateLimit } from 'express-rate-limit' - -dotenv.config() - -const app = express() - -// app.use( -// rateLimit({ -// windowMs: 1 * 60 * 1000, // 1 minuite -// max: 30, -// }) -// ) -app.use(cors()) -app.use(bodyParser.json({ limit: '30mb', extended: true })) -app.use(bodyParser.urlencoded({ limit: '30mb', extended: true })) - -app.use('/posts', PostRoutes) -app.use('/user', UserRoutes) -app.use('/comments', CommentRoutes) - -app.get('/', (_, res) => res.send('Hello to Memories API')) - -const PORT = process.env.PORT || 5000 -mongoose.set('strictQuery', true) -mongoose // https://www.mongodb.com/cloud/atlas - .connect(process.env.CONNECTION_URL) - .then(console.log('Connected to MongoDB Database 🌐')) - .then(() => app.listen(PORT, () => console.log(`Server running on port: ${PORT} πŸš€`))) - .catch((error) => console.log(`❎ Server did not connect ⚠️\n${error}`)) - -// CONFIGURE Connection URL: https://stackoverflow.com/questions/25090524/hide-mongodb-password-using-heroku-so-i-can-also-push-to-public-repo-on-github -// CONFIGURE AUTODEPLOY From Github: -// https://stackoverflow.com/questions/39197334/automated-heroku-deploy-from-subfolder -// https://github.com/timanovsky/subdir-heroku-buildpack diff --git a/server/middleware/auth.js b/server/middleware/auth.js deleted file mode 100644 index a64def57..00000000 --- a/server/middleware/auth.js +++ /dev/null @@ -1,26 +0,0 @@ -import jwt from 'jsonwebtoken' - -const secret = process.env.TOKEN_SECRET - -const auth = (req, _, next) => { - try { - const token = req.headers.authorization?.split(' ')[1] - const isCustomAuth = token?.length < 500 - - let decodedData = '' - - if (token && isCustomAuth) { - decodedData = jwt.verify(token, secret) - req.userId = decodedData?.id - } else { - decodedData = jwt.decode(token) - req.userId = decodedData?.sub.padStart(24, '0') - } - - next() - } catch (error) { - console.log(error) - } -} - -export default auth diff --git a/server/models/comment.js b/server/models/comment.js deleted file mode 100644 index 8bdf6f18..00000000 --- a/server/models/comment.js +++ /dev/null @@ -1,11 +0,0 @@ -import mongoose, { Schema } from 'mongoose' - -const commentSchema = Schema({ - post: mongoose.Types.ObjectId, - creator: mongoose.Types.ObjectId, - message: String, - likes: [String], - createdAt: { type: Date, default: new Date() }, -}) - -export default mongoose.model('comments', commentSchema) diff --git a/server/models/media.js b/server/models/media.js deleted file mode 100644 index 1ba1e7ba..00000000 --- a/server/models/media.js +++ /dev/null @@ -1,7 +0,0 @@ -import mongoose, { Schema } from 'mongoose' - -const mediaSchema = Schema({ - image: String -}) - -export default mongoose.model('media', mediaSchema) \ No newline at end of file diff --git a/server/models/post.js b/server/models/post.js deleted file mode 100644 index 08e45975..00000000 --- a/server/models/post.js +++ /dev/null @@ -1,14 +0,0 @@ -import mongoose, { Schema } from 'mongoose' - -const postSchema = Schema({ - creator: mongoose.Types.ObjectId, - title: String, - message: String, - thumbnail: String, - tags: [String], - likes: [String], - createdAt: { type: Date, default: new Date() }, - private: Boolean, -}) - -export default mongoose.model('posts', postSchema) diff --git a/server/models/user.js b/server/models/user.js deleted file mode 100644 index a57a319d..00000000 --- a/server/models/user.js +++ /dev/null @@ -1,12 +0,0 @@ -import mongoose, { Schema } from 'mongoose' - -const userSchema = Schema({ - name: { type: String, required: true }, - email: { type: String, required: true }, - resetToken: String, - password: String, - image: String, - avatar: Object, -}) - -export default mongoose.model('users', userSchema) diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index a135df26..00000000 --- a/server/package-lock.json +++ /dev/null @@ -1,1371 +0,0 @@ -{ - "name": "memories-server", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "memories-server", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "bcryptjs": "^2.4.3", - "body-parser": "^1.20.2", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "^7.4.0", - "jsonwebtoken": "^9.0.2", - "lodash": "^4.17.21", - "mongoose": "^8.6.1", - "nodemailer": "^6.9.15" - }, - "devDependencies": { - "nodemon": "^3.1.4" - } - }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", - "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dependencies": { - "@types/webidl-conversions": "*" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "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, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/bson": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", - "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", - "engines": { - "node": ">=16.20.1" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.2", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.6.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express-rate-limit": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.0.tgz", - "integrity": "sha512-v1204w3cXu5gCDmAvgvzI6qjzZzoMWKnyVDk3ACgfswTQLYiGen+r8w0VnXnGMmzEN/g8fwIQ4JrFFd4ZP6ssg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", - "dependencies": { - "jws": "^3.2.2", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/mongodb": { - "version": "6.8.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz", - "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.1.tgz", - "integrity": "sha512-dppGcYqvsdg+VcnqXR5b467V4a+iNhmvkfYNpEPi6AjaUxnz6ioEDmrMLOi+sOWjvoHapuwPOigV4f2l7HC6ag==", - "dependencies": { - "bson": "^6.7.0", - "kareem": "2.6.3", - "mongodb": "6.8.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/mquery/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mquery/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==" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/nodemailer": { - "version": "6.9.15", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", - "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/nodemon": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.4.tgz", - "integrity": "sha512-wjPBbFhtpJwmIeY2yP7QF+UKzPfltVGtfce1g/bB15/8vCGZj8uxD62b/b9M9/WVgme0NZudpownKN+c0plXlQ==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", - "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/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==", - "dev": true - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "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/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dependencies": { - "memory-pager": "^1.0.2" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", - "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/server/package.json b/server/package.json index cd50947c..4b927b28 100644 --- a/server/package.json +++ b/server/package.json @@ -1,29 +1,61 @@ { - "name": "memories-server", - "author": "warmachine028 ", - "version": "1.0.0", - "description": "This is the server for memories-client", - "private": true, - "main": "index.js", - "type": "module", - "scripts": { - "start": "nodemon index.js" - }, - "keywords": [], - "license": "ISC", - "dependencies": { - "bcryptjs": "^2.4.3", - "body-parser": "^1.20.2", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "^7.4.0", - "jsonwebtoken": "^9.0.2", - "lodash": "^4.17.21", - "mongoose": "^8.6.1", - "nodemailer": "^6.9.15" - }, - "devDependencies": { - "nodemon": "^3.1.4" - } + "$schema": "https://json.schemastore.org/package", + "name": "memories", + "type": "module", + "homepage": "https://github.com/warmachine028/memories#readme", + "version": "1.0.0", + "author": { + "name": "Pritam Kundu", + "email": "pritamkundu771@gmail.com", + "url": "https://github.com/warmachine028" + }, + "description": "A simple server to keep track of memories", + "bugs": { + "url": "https://github.com/warmachine028/memories/issues" + }, + "license": "MIT", + "scripts": { + "test": "bun --watch test", + "dev": "bun --watch src/index.ts", + "start": "bun run src/index.ts", + "postinstall": "prisma generate", + "prisma:generate": "prisma generate", + "prisma:push": "prisma db push", + "prisma:studio": "prisma studio", + "prisma:migrate": "prisma migrate dev --name init", + "prisma:deploy": "prisma migrate deploy" + }, + "dependencies": { + "@clerk/express": "^1.3.1", + "@elysiajs/cors": "^1.1.1", + "@elysiajs/cron": "^1.1.1", + "@elysiajs/swagger": "^1.1.5", + "@prisma/adapter-neon": "^5.21.1", + "@prisma/client": "^5.21.1", + "cloudinary": "^2.5.1", + "elysia": "latest", + "elysia-rate-limit": "^4.1.0", + "svix": "^1.37.0" + }, + "peerDependencies": { + "typescript": "^5.6.3" + }, + "devDependencies": { + "bun-types": "latest", + "prisma": "^5.21.1" + }, + "module": "src/index.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/warmachine028/memories.git" + }, + "keywords": [ + "memories", + "server", + "bun", + "typescript", + "elysia", + "cron", + "render" + ] } diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 00000000..5825f587 --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,138 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch", "driverAdapters", "tracing"] //? https://github.com/prisma/prisma/discussions/23533#discussioncomment-8838160 + //* https://www.prisma.io/blog/prisma-optimize-early-access?utm_source=cli&utm_medium=promo-generate-v5-17&utm_campaign=--optimize +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Visibility { + PUBLIC + PRIVATE +} + +model User { + id String @id @default(uuid()) + firstName String + lastName String? + email String @unique + bio String? + imageUrl String? + + posts Post[] + comments Comment[] + postReactions PostReaction[] + commentLikes CommentLike[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("users") +} + +model Post { + id String @id @default(uuid()) + authorId String + title String + description String + imageUrl String + visibility Visibility @default(PUBLIC) + reactionCount Int @default(0) + + tags PostTag[] + comments Comment[] + reactions PostReaction[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@map("posts") +} + +model Tag { + id String @id @default(uuid()) + name String @unique + + posts PostTag[] + + @@map("tags") +} + +model PostTag { + postId String + tagId String + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + tag Tag @relation(fields: [tagId], references: [id]) + + @@id(name: "postTagId", [postId, tagId]) + @@unique([postId, tagId]) + @@map("post_tags") +} + +model Comment { + id String @id @default(uuid()) + postId String + authorId String + content String + likeCount Int @default(0) + + likes CommentLike[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@map("comments") +} + +model PostReaction { + id String @id @default(uuid()) + userId String + postId String + reactionType ReactionType + + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) + + createdAt DateTime @default(now()) + + @@unique([userId, postId]) + @@map("post_reactions") +} + +model CommentLike { + userId String + commentId String + + comment Comment @relation(fields: [commentId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + createdAt DateTime @default(now()) + + @@id(name: "commentLikeId", [userId, commentId]) + @@unique([userId, commentId]) + @@map("comment_likes") +} + +enum ReactionType { + LIKE + LOVE + HAHA + WOW + SAD + ANGRY +} diff --git a/server/public/favicon.ico b/server/public/favicon.ico new file mode 100644 index 00000000..5b9f9387 Binary files /dev/null and b/server/public/favicon.ico differ diff --git a/server/routes/comments.js b/server/routes/comments.js deleted file mode 100644 index 16b227ce..00000000 --- a/server/routes/comments.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Router } from 'express' -import { createComment, deleteComment, getComments } from '../controllers/comments.js' -import auth from '../middleware/auth.js' - -const router = Router() - -router.get('/:id', auth, getComments) -router.post('/', auth, createComment) -router.delete('/:id', auth, deleteComment) - -export default router diff --git a/server/routes/index.js b/server/routes/index.js deleted file mode 100644 index 34995aed..00000000 --- a/server/routes/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import CommentRoutes from './comments.js' -import PostRoutes from './posts.js' -import UserRoutes from './users.js' - -export { CommentRoutes, PostRoutes, UserRoutes } diff --git a/server/routes/posts.js b/server/routes/posts.js deleted file mode 100644 index 8667c65f..00000000 --- a/server/routes/posts.js +++ /dev/null @@ -1,16 +0,0 @@ -import express from 'express' -import { getPostsBySearch, getPosts, getPost, createPost, updatePost, deletePost,getAllTags } from '../controllers/posts.js' -import auth from '../middleware/auth.js' - -const router = express.Router() - -// http://localhost:5000/posts/ -router.get('/', auth, getPosts) -router.get('/tags', auth, getAllTags) -router.get('/search', auth, getPostsBySearch) -router.get('/:id', auth, getPost) -router.post('/', auth, createPost) -router.delete('/:id', auth, deletePost) -router.patch('/:id', auth, updatePost) - -export default router diff --git a/server/routes/users.js b/server/routes/users.js deleted file mode 100644 index d825f9ee..00000000 --- a/server/routes/users.js +++ /dev/null @@ -1,20 +0,0 @@ -import express from 'express' -import { - getComments, - signin, signup, updateDetails, getUserDetails, getUserPostsByType, forgotPassword, resetPassword, googleSignin -} from '../controllers/user.js' -import auth from '../middleware/auth.js' - -const router = express.Router() - -router.post('/signin', signin) -router.post('/googlesignin', googleSignin) -router.post('/signup', signup) -router.post('/forgotPassword', forgotPassword) -router.post('/resetPassword', resetPassword) -router.patch('/update', updateDetails) -router.get('/details/:id', auth, getUserDetails) -router.get('/posts/:id', auth, getUserPostsByType) -router.get('/comments/:id', auth, getComments) - -export default router diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts new file mode 100644 index 00000000..2785a009 --- /dev/null +++ b/server/src/controllers/index.ts @@ -0,0 +1,3 @@ +export { getPosts, getPostById, createPost } from './post' +export { handleWebhook, getUsers, getUser } from './user' +export { react } from './reaction' diff --git a/server/src/controllers/post.ts b/server/src/controllers/post.ts new file mode 100644 index 00000000..3a4e4175 --- /dev/null +++ b/server/src/controllers/post.ts @@ -0,0 +1,132 @@ +import { prisma } from '@/lib' +import { deleteFromCloudinary, getPublicId, processPostsReactions, uploadToCloudinary } from '@/lib/utils' +import type { RequestParams } from '@/types' +import { error } from 'elysia' + +export const getPosts = async ({ query: { cursor, limit }, userId: currentUserId }: RequestParams) => { + const userId = currentUserId || '' + const posts = await prisma.post.findMany({ + include: { + author: { select: { fullName: true, imageUrl: true } }, + tags: { select: { tag: { select: { name: true } } } }, + reactions: { take: 1, where: { userId }, select: { reactionType: true } }, + }, + orderBy: { createdAt: 'desc' }, + take: (limit || 9) + 1, + where: { + OR: [{ visibility: 'PUBLIC' }, { visibility: 'PRIVATE', authorId: userId }], + }, + cursor: cursor ? { id: cursor } : undefined, + }) + const nextCursor = posts.length > limit ? posts[limit].id : undefined + return { posts: posts.slice(0, limit), nextCursor, total: await prisma.post.count() } +} + +export const getPostById = async ({ params: { id }, userId: currentUserId }: RequestParams) => { + const userId = currentUserId || '' + const post = await prisma.post.findUnique({ + where: { + id, + OR: [{ visibility: 'PUBLIC' }, { visibility: 'PRIVATE', authorId: userId }], + }, + include: { + author: { select: { fullName: true, imageUrl: true } }, + tags: { select: { tag: { select: { name: true } } } }, + reactions: true, + }, + }) + + if (!post) { + return error(404, { message: 'Post not found' }) + } + + const [processedPost] = processPostsReactions([post], userId) + return processedPost +} + +export const createPost = async ({ body, userId }: RequestParams) => { + if (!userId) { + return error(401, { message: 'Unauthorized' }) + } + const { title, description, visibility, tags, media } = body + const response = await uploadToCloudinary(media) + return prisma.post.create({ + data: { + title, + description, + imageUrl: response.secure_url, + visibility, + author: { connect: { id: userId } }, + tags: { + create: tags.map((name: string) => ({ + tag: { + connectOrCreate: { + where: { name }, + create: { name }, + }, + }, + })), + }, + }, + include: { + author: { select: { fullName: true, imageUrl: true } }, + tags: { include: { tag: { select: { name: true } } } }, + reactions: { take: 1, where: { userId }, select: { reactionType: true } }, + }, + }) +} + +export const deletePost = async ({ params: { id }, userId }: RequestParams) => { + if (!userId) { + return error(401, { message: 'Unauthorized' }) + } + const post = await prisma.post.delete({ + where: { id, authorId: userId }, + select: { + imageUrl: true, + }, + }) + if (!post.imageUrl) { + return error(404, { message: 'Post not found' }) + } + await deleteFromCloudinary(getPublicId(post.imageUrl) as string) + return { message: 'Post deleted successfully' } +} + +export const updatePost = async ({ params: { id }, body, userId }: RequestParams) => { + if (!userId) { + return error(401, { message: 'Unauthorized' }) + } + body.tags = body.tags.map(({ tag: { name } }: { tag: { name: string } }) => name) + const { title, description, visibility, tags, media, imageUrl } = body + const response = await uploadToCloudinary(media, getPublicId(imageUrl)) + + // STEP 1: Find the post and delete all the tag relations in PostTag + await prisma.postTag.deleteMany({ where: { postId: id } }) + + // STEP 2: Create the new tag relations in PostTag + return prisma.post.update({ + where: { id, authorId: userId }, + data: { + title, + description, + imageUrl: response.secure_url, + visibility, + tags: { + create: tags.map((name: string) => ({ + tag: { + connectOrCreate: { + where: { name }, + create: { name }, + }, + }, + })), + }, + }, + include: { + author: { select: { fullName: true, imageUrl: true } }, + tags: { include: { tag: { select: { name: true } } } }, + reactions: { take: 1, where: { userId }, select: { reactionType: true } }, + }, + }) +} diff --git a/server/src/controllers/reaction.ts b/server/src/controllers/reaction.ts new file mode 100644 index 00000000..ab660c00 --- /dev/null +++ b/server/src/controllers/reaction.ts @@ -0,0 +1,89 @@ +import { prisma } from '@/lib' +import { processPostsReactions } from '@/lib/utils' +import type { RequestParams } from '@/types' +import type { ReactionType } from '@prisma/client' + +export const react = async ({ params: { postId }, body, userId }: RequestParams) => { + if (!userId) { + throw new Error('Unauthorized') + } + + const { reactionType }: { reactionType: ReactionType } = body + + // Check if the post exists and is accessible to the user + const post = await prisma.post.findFirst({ + where: { + id: postId, + OR: [{ visibility: 'PUBLIC' }, { visibility: 'PRIVATE', authorId: userId }], + }, + }) + + if (!post) { + throw new Error('Post not found or not accessible') + } + + // Check if the user has already reacted to this post + const existingReaction = await prisma.postReaction.findUnique({ + where: { + userId_postId: { + userId, + postId, + }, + }, + }) + + let reaction + + if (existingReaction) { + if (existingReaction.reactionType === reactionType) { + // If the reaction is the same, remove it + await prisma.postReaction.delete({ + where: { id: existingReaction.id }, + }) + reaction = null + } else { + // If the reaction is different, update it + reaction = await prisma.postReaction.update({ + where: { id: existingReaction.id }, + data: { reactionType }, + }) + } + } else { + // If no existing reaction, create a new one + reaction = await prisma.postReaction.create({ + data: { + reactionType, + user: { connect: { id: userId } }, + post: { connect: { id: postId } }, + }, + }) + } + + // Fetch the updated post with reactions + const updatedPost = await prisma.post.findUnique({ + where: { id: postId }, + include: { + reactions: { + orderBy: { createdAt: 'desc' }, + take: 3, + select: { + reactionType: true, + userId: true, + createdAt: true, + user: { + select: { + imageUrl: true, + }, + }, + }, + }, + }, + }) + + if (!updatedPost) { + throw new Error('Failed to fetch updated post') + } + + const [processedPost] = processPostsReactions([updatedPost], userId) + return processedPost +} diff --git a/server/src/controllers/user.ts b/server/src/controllers/user.ts new file mode 100644 index 00000000..a9dd8220 --- /dev/null +++ b/server/src/controllers/user.ts @@ -0,0 +1,73 @@ +import { Webhook, WebhookRequiredHeaders } from 'svix' +import type { Event, EventType, RequestParams } from '@/types' +import { prisma } from '@/lib' + +export const handleWebhook = async ({ headers, request, set }: RequestParams) => { + const payload = await request.json() + const heads = { + 'svix-id': headers['svix-id'], + 'svix-timestamp': headers['svix-timestamp'], + 'svix-signature': headers['svix-signature'], + } + const wh = new Webhook(Bun.env.CLERK_WEBHOOK_SECRET as string) + let event: Event | null = null + + try { + event = wh.verify(JSON.stringify(payload), heads as WebhookRequiredHeaders) as Event + } catch (error) { + set.status = 400 + return { message: (error as Error).message } + } + + const eventType: EventType = event.type + + try { + switch (eventType) { + case 'user.created': + case 'user.updated': + case 'user.createdAtEdge': + await handleUserCreatedOrUpdated(event) + break + case 'user.deleted': + await handleUserDeleted(event) + break + default: + console.error(`Unhandled event type: ${eventType}`) + } + } catch (error) { + console.error('Error processing webhook:', error) + set.status = 500 + return { message: 'Error processing webhook', error: (error as Error).message } + } + + return { message: 'Webhook processed successfully', event } +} + +async function handleUserCreatedOrUpdated(event: Event) { + const { data } = event + const user = { + id: data.id, + firstName: data.first_name, + lastName: data.last_name, + email: data.email_addresses[0].email_address, + bio: data.unsafe_metadata.bio, + imageUrl: data.image_url, + } + + await prisma.user.upsert({ + where: { id: user.id }, + update: user, + create: user, + }) + + console.log(`User ${event.type}:`, user) +} + +async function handleUserDeleted(event: Event) { + const userId = event.data.id + await prisma.user.delete({ where: { id: userId } }) + console.log('User deleted:', userId) +} + +export const getUsers = async () => prisma.user.findMany() +export const getUser = ({ params: { id } }: RequestParams) => prisma.user.findUnique({ where: { id } }) diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 00000000..67fc232c --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,58 @@ +import { Elysia, t } from 'elysia' +import { rateLimit } from 'elysia-rate-limit' +import { swagger } from '@elysiajs/swagger' +import { cors } from '@elysiajs/cors' +import { cron } from '@elysiajs/cron' +import { postRoutes, commentRoutes, userRoutes, reactionRoutes, tagRoutes } from '@/routes' + +const port = Bun.env.PORT || 5000 +const RATE_LIMIT = 1000 +const RATE_LIMIT_WINDOW = 1000 * 60 // 1 minute in milliseconds + +new Elysia() + .use( + rateLimit({ + max: RATE_LIMIT, + duration: RATE_LIMIT_WINDOW, + }) + ) + .use( + // Create a cron job to ping the server every 14 minutes + cron({ + name: 'Ping Server', + pattern: '*/14 * * * *', + async run() { + try { + const response = await fetch('https://memories-omm3.onrender.com') + if (response.ok) { + console.log('Server pinged successfully') + } else { + console.error('Failed to ping server:', response.status, response.statusText) + } + } catch (error) { + console.error('Error pinging server:', error) + } + }, + }) + ) + .use(cors()) + .use( + swagger({ + path: '/docs', + documentation: { + info: { + title: 'Memories Documentation', + version: '1.0.0', + }, + }, + }) + ) + .get('/favicon.ico', () => Bun.file('public/favicon.ico')) + .get('/', () => 'πŸ’Ύ Hello from memories server') + .use(postRoutes) + .use(commentRoutes) + .use(userRoutes) + .use(reactionRoutes) + .use(commentRoutes) + .use(tagRoutes) + .listen(port, () => console.info(`🦊 Elysia is running at http://localhost:${port}`)) diff --git a/server/src/lib/cloudinary.ts b/server/src/lib/cloudinary.ts new file mode 100644 index 00000000..6aa23b18 --- /dev/null +++ b/server/src/lib/cloudinary.ts @@ -0,0 +1,10 @@ +import { v2 as cloudinary } from 'cloudinary' + +cloudinary.config({ + cloud_name: Bun.env.CLOUDINARY_CLOUD_NAME, + api_key: Bun.env.CLOUDINARY_API_KEY, + api_secret: Bun.env.CLOUDINARY_API_SECRET, + secure: true, +}); + +export default cloudinary diff --git a/server/src/lib/index.ts b/server/src/lib/index.ts new file mode 100644 index 00000000..43659b73 --- /dev/null +++ b/server/src/lib/index.ts @@ -0,0 +1,2 @@ +export { default as prisma } from './prisma' +export { default as cloudinary } from './cloudinary' diff --git a/server/src/lib/prisma.ts b/server/src/lib/prisma.ts new file mode 100644 index 00000000..055af84a --- /dev/null +++ b/server/src/lib/prisma.ts @@ -0,0 +1,31 @@ +//? https://github.com/prisma/prisma/discussions/23533#discussioncomment-8838160 +import { PrismaNeon } from '@prisma/adapter-neon' +import { Pool } from '@neondatabase/serverless' +import { PrismaClient } from '@prisma/client' + +const neon = new Pool({ connectionString: Bun.env.DATABASE_URL }) +const adapter = new PrismaNeon(neon) +const prismaClientSingleton = () => + new PrismaClient({ adapter }).$extends({ + result: { + user: { + fullName: { + needs: { firstName: true, lastName: true }, + compute(user) { + return `${user.firstName} ${user.lastName}` + }, + }, + }, + }, + }) + +declare global { + var prismaGlobal: undefined | ReturnType +} +const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() + +export default prisma + +if (process.env.NODE_ENV !== 'production') { + globalThis.prismaGlobal = prisma +} diff --git a/server/src/lib/utils.ts b/server/src/lib/utils.ts new file mode 100644 index 00000000..ecb3c3a3 --- /dev/null +++ b/server/src/lib/utils.ts @@ -0,0 +1,53 @@ +import type { ProcessedPostReaction } from '@/types' +import cloudinary from './cloudinary' + +// Helper function to process reactions +export const processPostsReactions = (posts: any[], userId: string | null) => { + return posts.map((post) => { + let currentUserReaction: ProcessedPostReaction | null = null + let otherReactions: ProcessedPostReaction[] = [] + + post.reactions.forEach((reaction: ProcessedPostReaction) => { + if (reaction.userId === userId) { + currentUserReaction = reaction + } else { + otherReactions.push(reaction) + } + }) + + const finalReactions = currentUserReaction ? [currentUserReaction, ...otherReactions.slice(0, 2)] : otherReactions.slice(0, 3) + + return { + ...post, + currentUserReaction, + recentReactions: finalReactions.map((r) => ({ + imageUrl: r.user.imageUrl, + createdAt: r.createdAt, + })), + } + }) +} + +export const uploadToCloudinary = (base64Image: string, public_id?: string) => { + try { + return cloudinary.uploader.upload(base64Image, { + resource_type: 'auto', + public_id, + overwrite: true, + }) + } catch (error) { + console.error('Error uploading to Cloudinary:', error) + throw error + } +} + +export const deleteFromCloudinary = (publicId: string) => { + try { + return cloudinary.uploader.destroy(publicId) + } catch (error) { + console.error('Error deleting from Cloudinary:', error) + throw error + } +} + +export const getPublicId = (imageUrl: string) => imageUrl.split('/').pop()?.split('.').shift() diff --git a/server/src/middlewares/auth.ts b/server/src/middlewares/auth.ts new file mode 100644 index 00000000..f97779a6 --- /dev/null +++ b/server/src/middlewares/auth.ts @@ -0,0 +1,26 @@ +import { verifyToken } from '@clerk/express' +import type { Elysia } from 'elysia' + +// Middleware to check authentication +export const authMiddleware = (app: Elysia) => + app.derive(async ({ set, headers }) => { + const sessionToken = headers['authorization']?.split(' ')[1] + + if (!sessionToken) { + return { userId: null } + } + + try { + const { userId } = await verifyToken(sessionToken, { + jwtKey: Bun.env.CLERK_JWT_TOKEN, + }) + if (!userId) { + set.status = 401 + return { userId: null } + } + return { userId } + } catch (error) { + set.status = 401 + return { userId: null, error: 'Invalid session' } + } + }) diff --git a/server/src/middlewares/index.ts b/server/src/middlewares/index.ts new file mode 100644 index 00000000..7a5a1697 --- /dev/null +++ b/server/src/middlewares/index.ts @@ -0,0 +1 @@ +export { authMiddleware } from './auth' diff --git a/server/src/routes/comment.ts b/server/src/routes/comment.ts new file mode 100644 index 00000000..f055a8b1 --- /dev/null +++ b/server/src/routes/comment.ts @@ -0,0 +1,82 @@ +import { Elysia, t } from 'elysia' +import type { RequestParams } from '@/types' +import { authMiddleware } from '@/middlewares' + +export const commentRoutes = new Elysia({ prefix: '/comments' }) // + .use(authMiddleware) + .guard({ + headers: t.Object({ + authorization: t.Optional(t.String()), + }), + }) + .get( + '/:postId', + ({ query: { postId } }: RequestParams) => { + return [ + { + id: '1', + postId: '1', + authorId: '1', + content: 'Hello', + likeCount: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '2', + postId: '1', + authorId: '2', + content: 'Hello', + likeCount: 0, + createdAt: new Date(), + updatedAt: new Date(), + }, + ].filter((comment) => comment.postId === postId) + }, + { + params: t.Object({ + postId: t.String(), + }), + } + ) + .guard({ + headers: t.Object({ + authorization: t.String(), + }), + }) + .post( + '/:postId', + ({ params: { postId }, body, userId }: RequestParams) => { + return { + id: '1', + postId, + authorId: userId, + content: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + } + }, + { + params: t.Object({ + postId: t.String(), + }), + } + ) + .delete('/:id', ({ params: { id } }: RequestParams) => { + return { + id, + postId: '1', + authorId: '1', + content: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + } + }) + .patch('/:id', ({ params: { id }, body, userId }: RequestParams) => { + return { + id, + postId: '1', + authorId: userId, + content: 'Hello', + } + }) diff --git a/server/src/routes/index.ts b/server/src/routes/index.ts new file mode 100644 index 00000000..f5adf93b --- /dev/null +++ b/server/src/routes/index.ts @@ -0,0 +1,5 @@ +export { postRoutes } from './post' +export { commentRoutes } from './comment' +export { userRoutes } from './user' +export { reactionRoutes } from './reaction' +export { tagRoutes } from './tags' diff --git a/server/src/routes/post.ts b/server/src/routes/post.ts new file mode 100644 index 00000000..ea336559 --- /dev/null +++ b/server/src/routes/post.ts @@ -0,0 +1,56 @@ +import { Elysia, t } from 'elysia' +import { getPosts, getPostById, createPost } from '@/controllers' +import { authMiddleware } from '@/middlewares' +import { Visibility } from '@prisma/client' +import type { RequestParams } from '@/types' +import { deletePost, updatePost } from '@/controllers/post' + +export const postRoutes = new Elysia({ prefix: '/posts' }) + .use(authMiddleware) + .guard({ + headers: t.Object({ + authorization: t.Optional(t.String()), + }), + }) + .get('/', getPosts, { + query: t.Optional( + t.Object({ + cursor: t.Optional(t.String()), + limit: t.Optional(t.Number()), + }) + ), + }) + .get('/:id', getPostById, { + params: t.Object({ + id: t.String(), + }), + }) + .guard({ + headers: t.Object({ + authorization: t.String(), + }), + }) + .post('/', createPost, { + body: t.Object({ + title: t.String(), + description: t.String(), + tags: t.Array(t.String()), + media: t.String(), + visibility: t.Enum(Visibility), + }), + }) + .put('/:id', updatePost, { + body: t.Object({ + title: t.String(), + description: t.String(), + tags: t.Array(t.Object({ tag: t.Object({ name: t.String() }) })), + media: t.String(), + imageUrl: t.String(), + visibility: t.Enum(Visibility), + }), + }) + .delete('/:id', deletePost, { + params: t.Object({ + id: t.String(), + }), + }) diff --git a/server/src/routes/reaction.ts b/server/src/routes/reaction.ts new file mode 100644 index 00000000..5ec60cc4 --- /dev/null +++ b/server/src/routes/reaction.ts @@ -0,0 +1,38 @@ +import { Elysia, t } from 'elysia' +import { react } from '@/controllers' +import { authMiddleware } from '@/middlewares' +import { ReactionType } from '@prisma/client' +import { RequestParams } from '@/types' + +export const reactionRoutes = new Elysia({ prefix: '/reactions' }) + .use(authMiddleware) + .get( + '/:postId', + ({ params: { postId } }: RequestParams) => { + return {} + }, + { + params: t.Object({ + postId: t.String(), + }), + } + ) + .guard({ + headers: t.Object({ + authorization: t.String(), + }), + }) + .post('/:postId', react, { + body: t.Object({ + reactionType: t.Enum(ReactionType), + }), + params: t.Object({ + postId: t.String(), + }), + }) + .delete('/:postId', ({ params: { postId } }: RequestParams) => { + return {} + }) + .patch('/:postId', ({ params: { postId } }: RequestParams) => { + return {} + }) diff --git a/server/src/routes/tags.ts b/server/src/routes/tags.ts new file mode 100644 index 00000000..86dec31c --- /dev/null +++ b/server/src/routes/tags.ts @@ -0,0 +1,50 @@ +import { Elysia, t } from 'elysia' +import type { RequestParams } from '@/types' + +export const tagRoutes = new Elysia({ prefix: '/tags' }) // + .get('/', () => { + return [ + { + id: '1', + name: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '2', + name: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '3', + name: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + }, + ] + }) + .get( + '/:postId', + ({ query: { name } }: RequestParams) => { + return [ + { + id: '1', + name: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + }, + { + id: '2', + name: 'Hello', + createdAt: new Date(), + updatedAt: new Date(), + }, + ].filter((tag) => tag.name === name) + }, + { + params: t.Object({ + postId: t.String(), + }), + } + ) \ No newline at end of file diff --git a/server/src/routes/user.ts b/server/src/routes/user.ts new file mode 100644 index 00000000..830c42c9 --- /dev/null +++ b/server/src/routes/user.ts @@ -0,0 +1,19 @@ +import { Elysia, t } from 'elysia' +import { handleWebhook, getUsers, getUser } from '@/controllers' + +const webhookRoutes = new Elysia({ prefix: '/webhook' }) + .guard({ + headers: t.Object({ + 'svix-id': t.String(), + 'svix-timestamp': t.String(), + 'svix-signature': t.String(), + }), + }) + .get('/', handleWebhook) + .post('/', handleWebhook) + .put('/', handleWebhook) + +export const userRoutes = new Elysia({ prefix: '/users' }) + .use(webhookRoutes) // + .get('/', getUsers) + .get('/:id', getUser) diff --git a/server/src/types.d.ts b/server/src/types.d.ts new file mode 100644 index 00000000..54a4ffeb --- /dev/null +++ b/server/src/types.d.ts @@ -0,0 +1,51 @@ +import { t } from 'elysia' +export type ReactionType = 'LIKE' | 'LOVE' | 'HAHA' | 'WOW' | 'SAD' | 'ANGRY' + +type EventEmailAddress = { + email_address: string + id: string + linked_to: any[] + object: 'email_address' + reserved: boolean + verification: { + attempts: null | number + expire_at: null | string + status: 'verified' | string + strategy: 'admin' | string + } +} +export type RequestParams = { + headers: Record + request: Request + set: { status: number } + params: { + id: string + postId: string + } + body: Record + userId: string | null + query: Record +} +export type EventType = 'user.created' | 'user.updated' | 'user.deleted' | 'user.createdAtEdge' +export type Event = { + data: { + id: string + first_name: string + last_name: null | string + email_addresses: EventEmailAddress[] + unsafe_metadata: Record + image_url: string + created_at: number + updated_at: number + } + object: 'event' + timestamp: number + type: EventType +} + +export type ProcessedPostReaction = { + userId: string + user: { imageUrl: string } + createdAt: Date + reactionType: ReactionType +} diff --git a/server/tests/ratelimit.test.ts b/server/tests/ratelimit.test.ts new file mode 100644 index 00000000..1637d743 --- /dev/null +++ b/server/tests/ratelimit.test.ts @@ -0,0 +1,40 @@ +import { test, expect, describe, beforeAll, afterAll } from 'bun:test' + +const PORT = Bun.env.PORT || 5000 +const BASE_URL = `http://localhost:${PORT}` +const RATE_LIMIT = 1000 +const RATE_LIMIT_WINDOW = 1000 * 60 // 1 minute in milliseconds + +describe('Rate Limit Tests', () => { + let startTime: number + + beforeAll(() => { + startTime = Date.now() + }) + + afterAll(() => { + const endTime = Date.now() + console.log(`Total test duration: ${(endTime - startTime) / 1000} seconds`) + }) + + test('should allow requests up to the rate limit', async () => { + for (let i = 0; i < RATE_LIMIT; i++) { + const response = await fetch(BASE_URL) + expect(response.status).toBe(200) + } + }) + + test('should reject requests exceeding the rate limit', async () => { + const response = await fetch(BASE_URL) + expect(response.status).toBe(429) + }) + + test('should reset rate limit after the time window', async () => { + // Wait for the rate limit window to pass + await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_WINDOW)) + + // Try a request after the window + const response = await fetch(BASE_URL) + expect(response.status).toBe(200) + }) +}) diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 00000000..038aed11 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,109 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + "types": ["bun-types"] /* Specify type package names to be included without being referenced in a source file. */, + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "baseUrl": "./", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/server/utils/emailSender.js b/server/utils/emailSender.js deleted file mode 100644 index 1a335ad4..00000000 --- a/server/utils/emailSender.js +++ /dev/null @@ -1,26 +0,0 @@ -import { createTransport } from 'nodemailer' - -const transport = createTransport({ - service: 'gmail', - auth: { - user: process.env.USER, - pass: process.env.PASS, - }, -}) - -export const sendEmail = (email, URL, res) => { - const mailOptions = { - from: `Memories Server πŸ‘» <${process.env.USER}>`, - to: email, - subject: 'Reset password link for memories', - text: `Click this link to reset your password - ${URL}`, - } - - transport.sendMail(mailOptions, (error, info) => { - if (error) { - return res.status(500).json({ message: 'Email was not sent', error: error.message }) - } - res.status(200).json(info) - }) -} diff --git a/server/vercel.json b/server/vercel.json deleted file mode 100644 index 8b205145..00000000 --- a/server/vercel.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": 2, - "builds": [ - { - "src": "./index.js", - "use": "@vercel/node" - } - ], - "routes": [ - { - "src": "/(.*)", - "dest": "/" - } - ] -} \ No newline at end of file