From 01a09fdec2cad6e1d636945667c61a0c62ac510c Mon Sep 17 00:00:00 2001 From: Sargam Date: Sat, 5 Oct 2024 18:45:56 +0545 Subject: [PATCH 01/13] chore: remove husky for now (#1157) --- .husky/post-merge | 4 ---- .husky/pre-commit | 12 ------------ package.json | 2 -- pnpm-lock.yaml | 24 +++++++----------------- 4 files changed, 7 insertions(+), 35 deletions(-) delete mode 100644 .husky/post-merge delete mode 100644 .husky/pre-commit diff --git a/.husky/post-merge b/.husky/post-merge deleted file mode 100644 index aa3a3a123..000000000 --- a/.husky/post-merge +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -git diff HEAD^ HEAD --exit-code -- ./package.json || npm install --legacy-peer-deps \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index d663f7203..000000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npm run format:fix -npm run lint:fix - -git add . - -if [ -f "yarn.lock" ] || [ -f "package-lock.json" ]; then - echo "Error: yarn.lock or package-lock.json is present. Please remove them before committing." - exit 1 -fi \ No newline at end of file diff --git a/package.json b/package.json index 8fe183237..0ac21ecd6 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "db:seed": "prisma db seed", "db:reset": "prisma migrate reset", "update:videometadata": "ts-node --compiler-options \"{\\\"module\\\": \\\"CommonJS\\\"}\" ./src/scripts/updateVideoMetaData.ts", - "prepare": "husky install", "studio": "prisma studio", "studio:docker": "open http://localhost:5555 || start http://localhost:5555", "storybook": "concurrently 'yarn:watch:*'", @@ -130,7 +129,6 @@ "autoprefixer": "^10.0.1", "eslint": "^8.56.0", "eslint-plugin-storybook": "^0.8.0", - "husky": "^9.0.7", "jsdom": "^24.0.0", "postcss": "^8", "prettier": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3eba56b84..a04131228 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -288,9 +288,6 @@ importers: eslint-plugin-storybook: specifier: ^0.8.0 version: 0.8.0(eslint@8.57.0)(typescript@5.5.4) - husky: - specifier: ^9.0.7 - version: 9.1.4 jsdom: specifier: ^24.0.0 version: 24.1.1 @@ -2381,13 +2378,13 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tabler/icons-react@3.19.0': - resolution: {integrity: sha512-AqEWGI0tQWgqo6ZjMO5yJ9sYT8oXLuAM/up0hN9iENS6IdtNZryKrkNSiMgpwweNTpl8wFFG/dAZ959S91A/uQ==} + '@tabler/icons-react@3.14.0': + resolution: {integrity: sha512-3XdbuyhBNq8aZW0qagR9YL8diACZYSAtaw6VuwcO2l6HzVFPN6N5TDex9WTz/3lf+uktAvOv1kNuuFBjSjN9yw==} peerDependencies: react: '>= 16' - '@tabler/icons@3.19.0': - resolution: {integrity: sha512-A4WEWqpdbTfnpFEtwXqwAe9qf9sp1yRPvzppqAuwcoF0q5YInqB+JkJtSFToCyBpPVeLxJUxxkapLvt2qQgnag==} + '@tabler/icons@3.14.0': + resolution: {integrity: sha512-OakKjK1kuDWKoNwdnHHVMt11kTZAC10iZpN/8o/CSYdeBH7S3v5n8IyqAYynFxLI8yBGTyBvljtvWdmWh57zSg==} '@testing-library/dom@10.1.0': resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} @@ -4363,11 +4360,6 @@ packages: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} - husky@9.1.4: - resolution: {integrity: sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==} - engines: {node: '>=18'} - hasBin: true - hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} @@ -9676,12 +9668,12 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tabler/icons-react@3.19.0(react@18.3.1)': + '@tabler/icons-react@3.14.0(react@18.3.1)': dependencies: - '@tabler/icons': 3.19.0 + '@tabler/icons': 3.14.0 react: 18.3.1 - '@tabler/icons@3.19.0': {} + '@tabler/icons@3.14.0': {} '@testing-library/dom@10.1.0': dependencies: @@ -12073,8 +12065,6 @@ snapshots: human-signals@5.0.0: {} - husky@9.1.4: {} - hyphenate-style-name@1.1.0: {} iconv-lite@0.4.24: From 694601728882505db4fd655c1199843aba09008f Mon Sep 17 00:00:00 2001 From: Piyush Waghela <83893659+noobpiyush@users.noreply.github.com> Date: Sun, 6 Oct 2024 02:12:45 +0530 Subject: [PATCH 02/13] fix: resolve hydration mismatch in layout component (#1436) >> >> - Add suppressHydrationWarning to html and body elements >> - Ensure consistent class naming using cn utility >> - Fix console warnings about extra class/style attributes >> >> This resolves the server/client rendering inconsistency and >> eliminates hydration warnings in the browser console. --- src/app/layout.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 733f37e35..976e9c458 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -24,8 +24,9 @@ export const metadata: Metadata = siteConfig; export default function RootLayout({ children }: { children: ReactNode }) { return ( - + Date: Sun, 6 Oct 2024 02:13:13 +0530 Subject: [PATCH 03/13] FEAT: Imporve the Accessibilty and Visibility of Question Tags (#1432) --- src/components/posts/tag.tsx | 16 +++++++++---- src/hooks/useColorGenerator.ts | 42 ++++++++++++++++------------------ 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/components/posts/tag.tsx b/src/components/posts/tag.tsx index 1aeeaa7db..ff260278c 100644 --- a/src/components/posts/tag.tsx +++ b/src/components/posts/tag.tsx @@ -1,4 +1,5 @@ 'use client'; + import useColorGenerator from '@/hooks/useColorGenerator'; import { cn } from '@/lib/utils'; import React, { forwardRef, Ref } from 'react'; @@ -15,8 +16,10 @@ const Tag = forwardRef( const [backgroundColor, textColor] = useColorGenerator(name); const tagClassName = cn( - 'px-4 rounded-xl py-1 text-[12px] cursor-pointer mr-1', - className, + 'inline-flex items-center px-2.5 py-1 rounded-lg text-sm font-semibold', + 'transition-colors duration-150 ease-in-out', + 'hover:opacity-90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-opacity-50', + className ); return ( @@ -24,7 +27,12 @@ const Tag = forwardRef( ref={ref} className={tagClassName} {...props} - style={{ backgroundColor, color: textColor }} + style={{ + backgroundColor, + color: textColor, + }} + role="status" + aria-label={`Tag: ${name}`} > {name} @@ -34,4 +42,4 @@ const Tag = forwardRef( Tag.displayName = 'Tag'; -export default Tag; +export default Tag; \ No newline at end of file diff --git a/src/hooks/useColorGenerator.ts b/src/hooks/useColorGenerator.ts index b3ca1e2a7..55a9bd043 100644 --- a/src/hooks/useColorGenerator.ts +++ b/src/hooks/useColorGenerator.ts @@ -1,36 +1,34 @@ 'use client'; + import { useCallback, useEffect, useState } from 'react'; +const tagColors = [ + { bg: '#F3F4F6', text: '#1F2937' }, // Gray + { bg: '#FEE2E2', text: '#991B1B' }, // Red + { bg: '#FEF3C7', text: '#92400E' }, // Yellow + { bg: '#D1FAE5', text: '#065F46' }, // Green + { bg: '#DBEAFE', text: '#1E40AF' }, // Blue + { bg: '#E0E7FF', text: '#3730A3' }, // Indigo + { bg: '#EDE9FE', text: '#5B21B6' }, // Purple + { bg: '#FCE7F3', text: '#9D174D' }, // Pink +]; + const useColorGenerator = (name: string = 'M1000'): [string, string] => { const [colors, setColors] = useState<[string, string]>(['', '']); - const stringToHexColor = (str: string): string => { + const generateColorIndex = useCallback((str: string): number => { let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } - - const color = (hash & 0x00ffffff).toString(16); - return `#${'00000'.substring(0, 6 - color.length) + color}`; - }; - - const isColorDark = (color: string): boolean => { - const hex = color.replace('#', ''); - const r = parseInt(hex.substring(0, 2), 16); - const g = parseInt(hex.substring(2, 2), 16); - const b = parseInt(hex.substring(4, 2), 16); - const brightness = (r * 299 + g * 587 + b * 114) / 1000; - return brightness < 128; - }; + return Math.abs(hash) % tagColors.length; + }, []); const updateColor = useCallback(() => { - const hexColor = stringToHexColor(name); - const isDark = isColorDark(hexColor); - const textColor = - // eslint-disable-next-line no-nested-ternary - name.split(' ').length === 1 ? '#ffffff' : isDark ? '#ffffff' : '#000000'; - setColors([hexColor, textColor]); - }, [name]); + const colorIndex = generateColorIndex(name); + const { bg, text } = tagColors[colorIndex]; + setColors([bg, text]); + }, [name, generateColorIndex]); useEffect(() => { updateColor(); @@ -39,4 +37,4 @@ const useColorGenerator = (name: string = 'M1000'): [string, string] => { return colors; }; -export default useColorGenerator; +export default useColorGenerator; \ No newline at end of file From f07964090d9a0630723b13b174881af1d4e9476d Mon Sep 17 00:00:00 2001 From: VineeTagarwaL-code Date: Mon, 7 Oct 2024 23:59:12 +0530 Subject: [PATCH 04/13] feat: added endpoint to access / sync users into job board --- src/app/api/user/exists/route.ts | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/app/api/user/exists/route.ts diff --git a/src/app/api/user/exists/route.ts b/src/app/api/user/exists/route.ts new file mode 100644 index 000000000..9254e8926 --- /dev/null +++ b/src/app/api/user/exists/route.ts @@ -0,0 +1,33 @@ +import { type NextRequest, NextResponse } from 'next/server'; +import db from '@/db'; + +export async function GET(req: NextRequest) { + const url = new URL(req.url); + + const apiKey = url.searchParams.get('apiKey'); + const email = url.searchParams.get('email'); + if (!apiKey) { + return NextResponse.json({ message: 'No token provided' }, { status: 400 }); + } + if (apiKey !== process.env.JOB_BOARD_AUTH_SECRET) { + return NextResponse.json({ message: 'Invalid token' }, { status: 400 }); + } + const user = await db.user.findFirst({ + where: { + email, + }, + select: { + purchases: true, + email: true, + id: true, + name: true, + certificate: true, + }, + }); + if (!user) { + return NextResponse.json({ message: 'User not found' }, { status: 404 }); + } + return NextResponse.json({ + user, + }); +} From 86bb20377b2d7fa6bdc7ae2f7bc2e67f1053eeda Mon Sep 17 00:00:00 2001 From: Hamzah Khan <66473175+hamzahshahbazkhan@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:29:40 +0530 Subject: [PATCH 05/13] Added checks so that comments are not blank. (#1441) * added checks for blank comments * added checks for blank comments --- src/components/comment/CommentInputForm.tsx | 33 ++++++++++++++++----- tsconfig.json | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/components/comment/CommentInputForm.tsx b/src/components/comment/CommentInputForm.tsx index 039209d7c..be991647e 100644 --- a/src/components/comment/CommentInputForm.tsx +++ b/src/components/comment/CommentInputForm.tsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button } from '../ui/button'; import { useAction } from '@/hooks/useAction'; import { createMessage } from '@/actions/comment'; @@ -15,6 +15,8 @@ const CommentInputForm = ({ parentId?: number | undefined; }) => { const currentPath = usePathname(); + const [isButtonDisabled, setButtonDisabled] = useState(true); + const [commentText, setCommentText] = useState(''); const formRef = React.useRef(null); const textareaRef = React.useRef(null); const { execute, isLoading, fieldErrors } = useAction(createMessage, { @@ -29,7 +31,6 @@ const CommentInputForm = ({ const handleFormSubmit = (e: React.FormEvent) => { e.preventDefault(); const formData = new FormData(e.target as HTMLFormElement); - const content = formData.get('content') as string; execute({ @@ -38,7 +39,14 @@ const CommentInputForm = ({ parentId, currentPath, }); + setCommentText(''); }; + + const isAllSpaces = (str: string): boolean => /^\s*$/.test(str); + + const isCommentValid = () => { + return !isAllSpaces(commentText); + } // Function to adjust the height of the textarea const adjustTextareaHeight = () => { @@ -48,6 +56,14 @@ const CommentInputForm = ({ } }; + useEffect(() => { + if (!isCommentValid() || isLoading) { + setButtonDisabled(true) + } else { + setButtonDisabled(false) + } + }, [commentText]) + // Effect to handle the initial and dynamic height adjustment useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -78,9 +94,6 @@ const CommentInputForm = ({ }; }, []); - const handleTextChange = () => { - adjustTextareaHeight(); - }; return (
{ + adjustTextareaHeight(); + setCommentText(e.target.value); + }} // Adjust height on text change /> - + ); }; diff --git a/tsconfig.json b/tsconfig.json index 88f05644b..3c4144d07 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,4 +26,4 @@ }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] -} +} \ No newline at end of file From 91cd7043b65174d663fd12f8850cdd481967b2e0 Mon Sep 17 00:00:00 2001 From: Chandragupt Singh Date: Wed, 9 Oct 2024 02:31:05 +0530 Subject: [PATCH 06/13] fix: common small bugs (#1461) Co-authored-by: Sargam --- src/components/videoPlayer/icons.tsx | 8 ++++---- tsconfig.json | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/videoPlayer/icons.tsx b/src/components/videoPlayer/icons.tsx index 6b297e4c9..ec64a8e5f 100644 --- a/src/components/videoPlayer/icons.tsx +++ b/src/components/videoPlayer/icons.tsx @@ -100,8 +100,8 @@ export const SkipDurationBackIcon = () => { height="20" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" + strokeWidth="2" + strokeLinecap="round" stroke-linejoin="round" > @@ -119,8 +119,8 @@ export const SkipDurationNextIcon = () => { height="20" fill="none" stroke="currentColor" - stroke-width="2" - stroke-linecap="round" + strokeWidth="2" + strokeLinecap="round" stroke-linejoin="round" > diff --git a/tsconfig.json b/tsconfig.json index 3c4144d07..380aa006a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,11 @@ "compilerOptions": { "baseUrl": ".", "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -12,7 +16,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ { @@ -20,10 +24,14 @@ } ], "paths": { - "@/*": ["./src/*"], - "@public/*": ["./public/*"] + "@/*": [ + "./src/*" + ], + "@public/*": [ + "./public/*" + ] } }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] -} \ No newline at end of file +} From 37fd577019b48479d40dc1a83759091858348145 Mon Sep 17 00:00:00 2001 From: Aryaman Gupta <144788392+Aryam2121@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:32:33 +0530 Subject: [PATCH 07/13] Update CourseCard.stories.ts (#1460) The discordOauthUrl field now contains the Discord invite link in all story objects.solve the issue #1437 --- .../stories/components/CourseCard.stories.ts | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/storybook/stories/components/CourseCard.stories.ts b/storybook/stories/components/CourseCard.stories.ts index 5d74b3fa6..80f9a5301 100644 --- a/storybook/stories/components/CourseCard.stories.ts +++ b/storybook/stories/components/CourseCard.stories.ts @@ -1,15 +1,3 @@ -import { CourseCard } from '@/components/CourseCard'; -import { Meta, StoryObj } from '@storybook/react'; - -const meta: Meta = { - title: 'CourseCard', - component: CourseCard, -}; - -export default meta; - -type Story = StoryObj; - export const ButtonColor: Story = { args: { course: { @@ -17,7 +5,7 @@ export const ButtonColor: Story = { slug: 'course-slug', appxCourseId: '1', certIssued: false, - discordOauthUrl: '', + discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link discordRoleId: 'discord-role-id', title: 'Course Title', description: 'Course Description', @@ -37,7 +25,7 @@ export const SmallRoundedCard: Story = { id: 1, slug: 'course-slug', appxCourseId: '1', - discordOauthUrl: '', + discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link certIssued: false, discordRoleId: 'discord-role-id', title: 'Course Title', @@ -58,8 +46,8 @@ export const MediumRoundedCard: Story = { id: 1, slug: 'course-slug', appxCourseId: '1', + discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link certIssued: false, - discordOauthUrl: '', discordRoleId: 'discord-role-id', title: 'Course Title', description: 'Course Description', @@ -79,9 +67,9 @@ export const LargeRoundedCard: Story = { id: 1, slug: 'course-slug', appxCourseId: '1', - discordOauthUrl: '', - discordRoleId: 'discord-role-id', + discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link certIssued: false, + discordRoleId: 'discord-role-id', title: 'Course Title', description: 'Course Description', imageUrl: From 551d14d9b1c547899ca7484fa8786cea03129c67 Mon Sep 17 00:00:00 2001 From: Faizan <101268983+Faizan711@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:33:56 +0530 Subject: [PATCH 08/13] fix: some common bugs in code (#1459) Co-authored-by: Sargam --- pnpm-lock.yaml | 357 ++++++++++++++++++++++++++- src/components/landing/footer.tsx | 2 +- src/components/videoPlayer/icons.tsx | 4 +- tsconfig.json | 11 +- 4 files changed, 367 insertions(+), 7 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a04131228..6a39d1ca3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,7 +33,7 @@ importers: specifier: ^1.0.4 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': - specifier: ^1.0.5 + specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 @@ -67,7 +67,7 @@ importers: version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tabler/icons-react': specifier: ^3.14.0 - version: 3.17.0(react@18.3.1) + version: 3.14.0(react@18.3.1) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -92,6 +92,9 @@ importers: clsx: specifier: ^2.1.0 version: 2.1.1 + cmdk: + specifier: 1.0.0 + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) date-fns: specifier: ^3.6.0 version: 3.6.0 @@ -1601,6 +1604,9 @@ packages: '@radix-ui/number@1.1.0': resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} @@ -1669,6 +1675,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-compose-refs@1.1.0': resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -1678,6 +1693,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.0': resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: @@ -1696,6 +1720,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.0.5': + resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dialog@1.1.1': resolution: {integrity: sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==} peerDependencies: @@ -1718,6 +1755,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dismissable-layer@1.0.5': + resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-dismissable-layer@1.1.0': resolution: {integrity: sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==} peerDependencies: @@ -1744,6 +1794,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-guards@1.1.0': resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} peerDependencies: @@ -1753,6 +1812,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-scope@1.0.4': + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-scope@1.1.0': resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} peerDependencies: @@ -1766,6 +1838,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1840,6 +1921,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.0.4': + resolution: {integrity: sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-portal@1.1.1': resolution: {integrity: sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==} peerDependencies: @@ -1853,6 +1947,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.0.1': + resolution: {integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.0': resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} peerDependencies: @@ -1879,6 +1986,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -1944,6 +2064,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -1979,6 +2108,15 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -1988,6 +2126,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-controllable-state@1.1.0': resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: @@ -1997,6 +2144,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: @@ -2006,6 +2162,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.0': resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: @@ -3304,6 +3469,12 @@ packages: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} + cmdk@1.0.0: + resolution: {integrity: sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==} + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -6016,6 +6187,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.5.7: resolution: {integrity: sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==} engines: {node: '>=10'} @@ -8699,6 +8880,10 @@ snapshots: '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/primitive@1.1.0': {} '@radix-ui/react-accordion@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -8767,12 +8952,26 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 @@ -8785,6 +8984,29 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dialog@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -8813,6 +9035,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -8841,12 +9077,31 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -8858,6 +9113,14 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-id@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-id@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -8963,6 +9226,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -8973,6 +9246,17 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -8993,6 +9277,16 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -9063,6 +9357,14 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -9105,12 +9407,27 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -9118,6 +9435,14 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1) @@ -9125,6 +9450,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.25.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1)': dependencies: react: 18.3.1 @@ -10793,6 +11125,16 @@ snapshots: cluster-key-slot@1.1.2: {} + cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -13913,6 +14255,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) + tslib: 2.6.3 + use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + react-remove-scroll@2.5.7(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/components/landing/footer.tsx b/src/components/landing/footer.tsx index 0c71c000d..49138b32a 100644 --- a/src/components/landing/footer.tsx +++ b/src/components/landing/footer.tsx @@ -77,7 +77,7 @@ const Footer = () => { { stroke="currentColor" strokeWidth="2" strokeLinecap="round" - stroke-linejoin="round" + strokeLinejoin="round" > @@ -121,7 +121,7 @@ export const SkipDurationNextIcon = () => { stroke="currentColor" strokeWidth="2" strokeLinecap="round" - stroke-linejoin="round" + strokeLinejoin="round" > diff --git a/tsconfig.json b/tsconfig.json index 380aa006a..adf349623 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,13 @@ ] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From f7e9a725e1c8aa88048e967d6bc950c67e790501 Mon Sep 17 00:00:00 2001 From: Soham Gupta <97831613+gupta-soham@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:34:58 +0530 Subject: [PATCH 09/13] Implement global search functionality (#1444) * feat(search): Implement global search functionality - Add global search in `Navbar` with optimizations (mobile-friendly) - Implement command menu for page navigation - Utilize debounce hook for optimized API calls - Refactor `SearchBar` component for reusability * Optimize icon determination and styling --- src/app/(main)/(pages)/home/page.tsx | 4 - src/components/Navbar.tsx | 78 +++++- src/components/search/CommandMenu.tsx | 194 ++++++++++++++ src/components/search/SearchBar.tsx | 311 ++++++++++++++-------- src/components/search/SearchResults.tsx | 59 ++++ src/components/search/VideoSearchCard.tsx | 22 +- src/hooks/useDebounce.ts | 17 ++ 7 files changed, 548 insertions(+), 137 deletions(-) create mode 100644 src/components/search/CommandMenu.tsx create mode 100644 src/components/search/SearchResults.tsx create mode 100644 src/hooks/useDebounce.ts diff --git a/src/app/(main)/(pages)/home/page.tsx b/src/app/(main)/(pages)/home/page.tsx index e624d043f..c251ae697 100644 --- a/src/app/(main)/(pages)/home/page.tsx +++ b/src/app/(main)/(pages)/home/page.tsx @@ -1,7 +1,6 @@ import { Greeting } from '@/components/Greeting'; import { MyCourses } from '@/components/MyCourses'; import { Redirect } from '@/components/Redirect'; -import { SearchBar } from '@/components/search/SearchBar'; import { getServerSession } from 'next-auth'; export default async function MyCoursesPage() { @@ -17,9 +16,6 @@ export default async function MyCoursesPage() {

{session.user.name}

-
- -
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 2caece7c8..b61debf0a 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -3,33 +3,42 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useSession } from 'next-auth/react'; import { usePathname, useRouter } from 'next/navigation'; -import { useState } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import Link from 'next/link'; -import { ArrowLeft, Menu } from 'lucide-react'; +import { ArrowLeft, Menu, Search, X } from 'lucide-react'; import { Button } from './ui/button'; import { AppbarAuth } from './AppbarAuth'; import { SelectTheme } from './ThemeToggler'; import ProfileDropdown from './profile-menu/ProfileDropdown'; +import { SearchBar } from './search/SearchBar'; export const Navbar = () => { const { data: session } = useSession(); const router = useRouter(); const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isSearchOpen, setIsSearchOpen] = useState(false); const pathname = usePathname(); - const toggleMenu = () => setIsMenuOpen(!isMenuOpen); - const navItemVariants = { - hidden: { opacity: 0, y: -20 }, - visible: (i: number) => ({ - opacity: 1, - y: 0, - transition: { - delay: i * 0.1, - duration: 0.5, - ease: [0.43, 0.13, 0.23, 0.96], - }, + // Memoizing the toggleMenu and toggleSearch functions + const toggleMenu = useCallback(() => setIsMenuOpen((prev) => !prev), []); + const toggleSearch = useCallback(() => setIsSearchOpen((prev) => !prev), []); + + // Memoizing the navItemVariants object + const navItemVariants = useMemo( + () => ({ + hidden: { opacity: 0, y: -20 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { + delay: i * 0.1, + duration: 0.5, + ease: [0.43, 0.13, 0.23, 0.96], + }, + }), }), - }; + [], + ); return ( @@ -85,6 +94,25 @@ export const Navbar = () => { variants={navItemVariants} custom={1} > + {session?.user && ( + <> +
+ +
+ +
+ +
+ + )} + {session?.user && } @@ -145,6 +173,28 @@ export const Navbar = () => { )} + + {/* Mobile search overlay */} + + {isSearchOpen && ( + +
+

Search

+ + +
+ + +
+ )} +
); }; diff --git a/src/components/search/CommandMenu.tsx b/src/components/search/CommandMenu.tsx new file mode 100644 index 000000000..f0bf51e63 --- /dev/null +++ b/src/components/search/CommandMenu.tsx @@ -0,0 +1,194 @@ +'use client'; + +import { TSearchedVideos } from '@/app/api/search/route'; +import { + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from '@/components/ui/command'; +import { SiGithub, SiNotion } from '@icons-pack/react-simple-icons'; +import { + Bookmark, + Calendar, + History, + LogOut, + MessageCircleQuestion, + NotebookPen, + NotebookText, + Play, +} from 'lucide-react'; +import { signOut } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; +import { useCallback, useEffect } from 'react'; + +interface CommandMenuProps { + icon: string; + open: boolean; + onOpenChange: (open: boolean) => void; + commandSearchTerm: string; + onCommandSearchTermChange: (value: string) => void; + loading: boolean; + searchedVideos: TSearchedVideos[] | null; + onCardClick: (videoUrl: string) => void; + onClose: () => void; +} + +export function CommandMenu({ + icon, + open, + onOpenChange, + commandSearchTerm, + onCommandSearchTermChange, + loading, + searchedVideos, + onCardClick, + onClose, +}: CommandMenuProps) { + const router = useRouter(); + + const handleShortcut = useCallback( + (route: string) => { + if (route.startsWith('http')) { + window.location.href = route; + } else { + router.push(route); + } + onClose(); + }, + [router, onClose], + ); + + // Shortcut Handlers + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (open && e.ctrlKey) { + const shortcuts = { + c: '/home', + h: '/watch-history', + b: '/bookmark', + d: '/question', + s: 'https://projects.100xdevs.com/', + g: 'https://github.com/code100x/', + }; + + const key = e.key.toLowerCase() as keyof typeof shortcuts; + if (shortcuts[key]) { + e.preventDefault(); + handleShortcut(shortcuts[key]); + } + } + }, + [open, handleShortcut], + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + return ( + + + + No results found. + + + {!loading && + searchedVideos && + searchedVideos.length > 0 && + searchedVideos.map((video) => ( + { + if (video.parentId && video.parent?.courses.length) { + const courseId = video.parent.courses[0].courseId; + const videoUrl = `/courses/${courseId}/${video.parentId}/${video.id}`; + onCardClick(videoUrl); + onClose(); + } + }} + > + + {video.title} + + ))} + {!loading && (!searchedVideos || searchedVideos.length === 0) && ( + No videos found + )} + + + + + + handleShortcut('/calendar')}> + + Calendar + + + handleShortcut('https://github.com/100xdevs-cohort-3/assignments') + } + > + + Cohort 3 Assignments + + { + signOut(); + onClose(); + }} + > + + Log Out + + + + + handleShortcut('/home')}> + + Courses + {icon}C + + handleShortcut('/watch-history')}> + + Watch History + {icon}H + + handleShortcut('/bookmark')}> + + Bookmarks + {icon}B + + handleShortcut('/question')}> + + Questions + {icon}D + + handleShortcut('https://projects.100xdevs.com/')} + > + + Slides + {icon}S + + handleShortcut('https://github.com/code100x/')} + > + + Contribute to code100x + {icon}G + + + + + ); +} diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 1fbc5590c..cbc7cd2cb 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -1,146 +1,221 @@ 'use client'; -import React, { useCallback, useEffect, useState } from 'react'; + import { TSearchedVideos } from '@/app/api/search/route'; +import useClickOutside from '@/hooks/useClickOutside'; +import { useDebounce } from '@/hooks/useDebounce'; +import { SearchIcon } from 'lucide-react'; import { useRouter } from 'next/navigation'; -import VideoSearchCard from './VideoSearchCard'; -import VideoSearchInfo from './VideoSearchInfo'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { toast } from 'sonner'; -import VideoSearchLoading from './VideoSearchLoading'; -import { - Command, - CommandDialog, - CommandInput, - CommandList, -} from '../ui/command'; - -export function SearchBar() { - const [searchTerm, setSearchTerm] = useState(''); - const [searchedVideos, setSearchedVideos] = useState([]); - const [loading, setLoading] = useState(false); - const [dialogOpen, setDialogOpen] = useState(false); - const [selectedIndex, setSelectedIndex] = useState(-1); +import { Input } from '../ui/input'; +import { CommandMenu } from './CommandMenu'; +import { SearchResults } from './SearchResults'; + +interface SearchBarProps { + onCardClick?: () => void; + isMobile?: boolean; +} + +export function SearchBar({ onCardClick, isMobile = false }: SearchBarProps) { + const [state, setState] = useState({ + open: false, + searchTerm: '', + commandSearchTerm: '', + searchedVideos: null as TSearchedVideos[] | null, + isInputFocused: false, + loading: false, + selectedIndex: -1, + }); + + const debouncedSearchTerm = useDebounce(state.searchTerm, 300); + const debouncedCommandSearchTerm = useDebounce(state.commandSearchTerm, 300); + + const ref = useRef(null); const router = useRouter(); - const fetchData = useCallback(async (searchTerm: string) => { - setLoading(true); + useClickOutside(ref, () => { + setState((prev) => ({ ...prev, isInputFocused: false })); + }); + + const fetchData = useCallback(async (term: string) => { + setState((prev) => ({ ...prev, loading: true })); + try { - const response = await fetch(`/api/search?q=${searchTerm}`); + const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } const data = await response.json(); - setSearchedVideos(data); + setState((prev) => ({ ...prev, searchedVideos: data, loading: false })); } catch (err) { toast.error('Something went wrong while searching for videos'); - setSearchTerm(''); - } finally { - setLoading(false); + setState((prev) => ({ + ...prev, + commandSearchTerm: '', + searchTerm: '', + searchedVideos: null, + loading: false, + })); } }, []); useEffect(() => { - if (searchTerm.trimEnd().length > 2) { - const timeoutId = setTimeout(() => { - fetchData(searchTerm); - }, 300); - - return () => clearTimeout(timeoutId); + if (debouncedSearchTerm.trim().length > 2) { + fetchData(debouncedSearchTerm); + } else { + setState((prev) => ({ ...prev, searchedVideos: null })); } - setSearchedVideos([]); - }, [searchTerm, fetchData]); + }, [debouncedSearchTerm, fetchData]); useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - // Check for Ctrl+K on Windows/Linux or Cmd+K on macOS - if ( - (event.ctrlKey && event.key === 'k') || - (event.metaKey && event.key === 'k') - ) { - event.preventDefault(); - setDialogOpen((prev) => !prev); + if (debouncedCommandSearchTerm.trim().length > 2) { + fetchData(debouncedCommandSearchTerm); + } else { + setState((prev) => ({ ...prev, searchedVideos: null })); + } + }, [debouncedCommandSearchTerm, fetchData]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setState((prev) => ({ ...prev, open: !prev.open })); } - // For control of search result - switch (event.code) { - case 'ArrowDown': - event.preventDefault(); - setSelectedIndex( - (prevIndex) => (prevIndex + 1) % searchedVideos.length, - ); - break; - case 'ArrowUp': - event.preventDefault(); - setSelectedIndex( - (prevIndex) => - (prevIndex - 1 + searchedVideos.length) % searchedVideos.length, - ); - break; - case 'Enter': - if (selectedIndex !== -1) { - event.preventDefault(); - const { - id: videoId, - parentId, - parent, - } = searchedVideos[selectedIndex]; - - if (parentId && parent?.courses.length) { - const courseId = parent.courses[0].courseId; - const videoUrl = `/courses/${courseId}/${parentId}/${videoId}`; - router.push(videoUrl); + + if (state.open) { + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setState((prev) => ({ + ...prev, + selectedIndex: + (prev.selectedIndex + 1) % (prev.searchedVideos?.length || 0), + })); + break; + case 'ArrowUp': + e.preventDefault(); + setState((prev) => ({ + ...prev, + selectedIndex: + (prev.selectedIndex - 1 + (prev.searchedVideos?.length || 0)) % + (prev.searchedVideos?.length || 0), + })); + break; + case 'Enter': + e.preventDefault(); + if (state.selectedIndex !== -1 && state.searchedVideos) { + const selectedVideo = state.searchedVideos[state.selectedIndex]; + if ( + selectedVideo.parentId && + selectedVideo.parent?.courses.length + ) { + const courseId = selectedVideo.parent.courses[0].courseId; + const videoUrl = `/courses/${courseId}/${selectedVideo.parentId}/${selectedVideo.id}`; + router.push(videoUrl); + setState((prev) => ({ ...prev, open: false })); + } } - } - break; - default: - break; + break; + } } - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [searchedVideos, selectedIndex]); - - const renderSearchResults = () => { - if (searchTerm.length < 3) { - return ( - - ); - } else if (loading) { - return ; - } else if (!searchedVideos || searchedVideos.length === 0) { - return ; - } - return searchedVideos.map((video, index) => ( -
- -
- )); - }; + }, + [state.open, state.selectedIndex, state.searchedVideos, router], + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + const handleInputChange = useCallback( + (event: React.ChangeEvent) => { + setState((prev) => ({ ...prev, searchTerm: event.target.value })); + }, + [], + ); + + const handleCardClick = useCallback( + (videoUrl: string) => { + if (onCardClick) { + onCardClick(); + } + setState((prev) => ({ ...prev, searchTerm: '', commandSearchTerm: '' })); + router.push(videoUrl); + }, + [onCardClick, router], + ); + + const icon = useMemo(() => { + return navigator.userAgent.toLowerCase().includes('mac') ? '⌘' : 'Ctrl + '; + }, []); return ( <> - setDialogOpen(true)}> -
- setSearchTerm(search)} - /> - - ⌘{' '} - K - -
- -
- setDialogOpen((prev) => !prev)} +
- setSearchTerm(search)} + {/* Search Input */} + + + setState((prev) => ({ ...prev, isInputFocused: true })) + } + aria-label="Search" + /> + {state.searchTerm.length === 0 && + !isMobile && + (icon !== '⌘' ? ( + + {icon}K + + ) : ( + + {icon}K + + ))} + + 0 + } + isMobile={isMobile} + searchTerm={state.searchTerm} + loading={state.loading} + searchedVideos={state.searchedVideos} + selectedIndex={state.selectedIndex} + onCardClick={handleCardClick} + /> +
+ + {!isMobile && ( + setState((prev) => ({ ...prev, open }))} + commandSearchTerm={state.commandSearchTerm} + onCommandSearchTermChange={(value) => + setState((prev) => ({ ...prev, commandSearchTerm: value })) + } + loading={state.loading} + searchedVideos={state.searchedVideos} + onCardClick={handleCardClick} + onClose={() => setState((prev) => ({ ...prev, open: false }))} /> - {renderSearchResults()} -
+ )} ); } diff --git a/src/components/search/SearchResults.tsx b/src/components/search/SearchResults.tsx new file mode 100644 index 000000000..fbef8c12d --- /dev/null +++ b/src/components/search/SearchResults.tsx @@ -0,0 +1,59 @@ +import { TSearchedVideos } from '@/app/api/search/route'; +import VideoSearchCard from './VideoSearchCard'; +import VideoSearchInfo from './VideoSearchInfo'; +import VideoSearchLoading from './VideoSearchLoading'; + +interface SearchResultsProps { + isVisible: boolean; + isMobile: boolean; + searchTerm: string; + loading: boolean; + searchedVideos: TSearchedVideos[] | null; + selectedIndex: number; + onCardClick: (videoUrl: string) => void; +} + +export function SearchResults({ + isVisible, + isMobile, + searchTerm, + loading, + searchedVideos, + selectedIndex, + onCardClick, +}: SearchResultsProps) { + if (!isVisible) return null; + + const renderSearchResults = () => { + if (searchTerm.length < 3) { + return ( + + ); + } + if (loading) { + return ; + } + if (!searchedVideos || searchedVideos.length === 0) { + return ; + } + + return searchedVideos.map((video, index) => ( +
+ +
+ )); + }; + + return ( +
+ {renderSearchResults()} +
+ ); +} diff --git a/src/components/search/VideoSearchCard.tsx b/src/components/search/VideoSearchCard.tsx index 888d71a90..c95c962b3 100644 --- a/src/components/search/VideoSearchCard.tsx +++ b/src/components/search/VideoSearchCard.tsx @@ -3,15 +3,33 @@ import { Play } from 'lucide-react'; import Link from 'next/link'; import React from 'react'; -const VideoSearchCard = ({ video }: { video: TSearchedVideos }) => { +interface VideoSearchCardProps { + video: TSearchedVideos; + onCardClick?: (videoUrl: string) => void; +} + +const VideoSearchCard: React.FC = ({ + video, + onCardClick, +}) => { const { id: videoId, parentId, parent } = video; if (parentId && parent?.courses.length) { const courseId = parent.courses[0].courseId; const videoUrl = `/courses/${courseId}/${parentId}/${videoId}`; + + // Customizable click handler which allows parent components to override default navigation behavior + const handleClick = (e: React.MouseEvent) => { + if (onCardClick) { + e.preventDefault(); + onCardClick(videoUrl); + } + }; + return ( @@ -20,6 +38,8 @@ const VideoSearchCard = ({ video }: { video: TSearchedVideos }) => { ); } + + return null; }; export default VideoSearchCard; diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 000000000..0991ca4b0 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + + return debouncedValue; +} From 9da279634bfaa9f0d956d7e3e4eec48068272817 Mon Sep 17 00:00:00 2001 From: Shawn Dsilva <107312993+zzzzshawn@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:37:02 +0530 Subject: [PATCH 10/13] Feature/theme change transition (#1445) * Added animatiions while theme change * bookmark Co-authored-by: Sargam --- public/NoBookmark.svg | 9 ++++ src/app/globals.css | 25 +++++++++++ src/components/Navbar.tsx | 23 +--------- src/components/ThemeToggler.tsx | 57 +++++++++++++----------- src/components/bookmark/BookmarkView.tsx | 5 +-- src/components/bookmark/NoBookmark.tsx | 27 +++++++++++ 6 files changed, 97 insertions(+), 49 deletions(-) create mode 100644 public/NoBookmark.svg create mode 100644 src/components/bookmark/NoBookmark.tsx diff --git a/public/NoBookmark.svg b/public/NoBookmark.svg new file mode 100644 index 000000000..0c98ab085 --- /dev/null +++ b/public/NoBookmark.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/app/globals.css b/src/app/globals.css index 7f2731179..7db494853 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -330,4 +330,29 @@ /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { background: #555; +} + +::view-transition-group(root) { + animation-timing-function: var(--expo-out); +} + +::view-transition-new(root) { + mask: url('data:image/svg+xml,') + top left / 0 no-repeat; + mask-origin: content-box; + animation: scale 1s; + transform-origin: top left; +} + +::view-transition-old(root), +.dark::view-transition-old(root) { + animation: scale 1s; + transform-origin: top left; + z-index: -1; +} + +@keyframes scale { + to { + mask-size: 350vmax; + } } \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b61debf0a..b3841c077 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -8,7 +8,7 @@ import Link from 'next/link'; import { ArrowLeft, Menu, Search, X } from 'lucide-react'; import { Button } from './ui/button'; import { AppbarAuth } from './AppbarAuth'; -import { SelectTheme } from './ThemeToggler'; +import ThemeToggler from './ThemeToggler'; import ProfileDropdown from './profile-menu/ProfileDropdown'; import { SearchBar } from './search/SearchBar'; @@ -94,26 +94,7 @@ export const Navbar = () => { variants={navItemVariants} custom={1} > - {session?.user && ( - <> -
- -
- -
- -
- - )} - - + {session?.user && } {!session?.user && ( diff --git a/src/components/ThemeToggler.tsx b/src/components/ThemeToggler.tsx index 2af4772ad..88e613d88 100644 --- a/src/components/ThemeToggler.tsx +++ b/src/components/ThemeToggler.tsx @@ -1,33 +1,40 @@ 'use client'; -import * as React from 'react'; import { Moon, Sun } from 'lucide-react'; import { useTheme } from 'next-themes'; +import { Button } from './ui/button'; + +export default function ThemeToggler() { + const { theme, setTheme } = useTheme(); + + const switchTheme = () => { + switch (theme) { + case 'light': + setTheme('dark'); + break; + case 'dark': + setTheme('light'); + break; + default: + break; + } + }; + + const toggleTheme = () => { + if (!document.startViewTransition) switchTheme(); + + document.startViewTransition(switchTheme); + }; -export function SelectTheme({ text }: { text?: boolean }) { - const { setTheme, theme } = useTheme(); return ( - <> - {text === false ? ( - - ) : ( - - )} - + ); } diff --git a/src/components/bookmark/BookmarkView.tsx b/src/components/bookmark/BookmarkView.tsx index aa78a786a..6f5705016 100644 --- a/src/components/bookmark/BookmarkView.tsx +++ b/src/components/bookmark/BookmarkView.tsx @@ -1,5 +1,6 @@ import BookmarkList from './BookmarkList'; import { TBookmarkWithContent } from '@/actions/bookmark/types'; +import NoBookmark from './NoBookmark'; const BookmarkView = ({ bookmarkData, @@ -11,9 +12,7 @@ const BookmarkView = ({ {bookmarkData === null || 'error' in bookmarkData || !bookmarkData.length ? ( -
-
No bookmark added yet!
-
+ ) : ( )} diff --git a/src/components/bookmark/NoBookmark.tsx b/src/components/bookmark/NoBookmark.tsx new file mode 100644 index 000000000..4ea68e8ce --- /dev/null +++ b/src/components/bookmark/NoBookmark.tsx @@ -0,0 +1,27 @@ +import { Bookmark } from 'lucide-react'; +import Image from 'next/image'; +import React from 'react'; + +const NoBookmark = () => { + return ( +
+ No Bookmarks +

+ Well.. You have'nt Bookmarked anything yet.... +

+

+ 💡When you find something you want to save for later, Click the “ + + ” icon and it will appear here. +

+
+ ); +}; + +export default NoBookmark; From e0b174f5bbf5169e44272f832f1519897ac82bda Mon Sep 17 00:00:00 2001 From: Karthik J <114949890+Karthik150502@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:38:48 +0530 Subject: [PATCH 11/13] Added a shadow to the PostCard component. (#1435) --- src/components/posts/PostCard.tsx | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/components/posts/PostCard.tsx b/src/components/posts/PostCard.tsx index e4cc4486d..bef916a81 100644 --- a/src/components/posts/PostCard.tsx +++ b/src/components/posts/PostCard.tsx @@ -99,11 +99,10 @@ const PostCard: React.FC = ({ return (
{ startTransition(() => { if (isExtendedQuestion(post)) { @@ -132,12 +131,12 @@ const PostCard: React.FC = ({
{(sessionUser?.role === ROLES.ADMIN || post?.author?.id === sessionUser?.id) && ( - - )} + + )}
{parentAuthorName && isAnswer && ( @@ -216,10 +215,10 @@ const PostCard: React.FC = ({ {enableReply && ( -
= ({ sessionUser={sessionUser} reply={false} parentAuthorName={post.author.name} - isAnswer={true} /> + isAnswer={true} />
))} From a58690c0eaf649287d36bad07901bc298ee0f553 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi <90623311+Jatin123lodhi@users.noreply.github.com> Date: Wed, 9 Oct 2024 02:40:17 +0530 Subject: [PATCH 12/13] show content titles on hover of weekly cards (#1429) --- src/components/ContentCard.tsx | 134 ++++++++++++++++++--------------- src/components/FolderView.tsx | 5 +- src/lib/utils.ts | 2 + 3 files changed, 80 insertions(+), 61 deletions(-) diff --git a/src/components/ContentCard.tsx b/src/components/ContentCard.tsx index 87345a185..8c8a76c4b 100644 --- a/src/components/ContentCard.tsx +++ b/src/components/ContentCard.tsx @@ -5,6 +5,7 @@ import { formatTime } from '@/lib/utils'; import VideoThumbnail from './videothumbnail'; import CardComponent from './CardComponent'; import { motion } from 'framer-motion'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; export const ContentCard = ({ title, @@ -15,6 +16,7 @@ export const ContentCard = ({ bookmark, contentId, contentDuration, + weeklyContentTitles, }: { type: 'folder' | 'video' | 'notion'; contentId?: number; @@ -27,68 +29,80 @@ export const ContentCard = ({ bookmark?: Bookmark | null; contentDuration?: number; uploadDate?: string; + weeklyContentTitles?: string[]; }) => { return ( - (['Enter', ' '].includes(e.key) && onClick())} - className={`group relative flex h-fit w-full max-w-md cursor-pointer flex-col gap-2 rounded-2xl transition-all duration-300 hover:-translate-y-2`} - > - {markAsCompleted && ( -
- -
- )} - {type === 'video' && ( -
- -
- )} - {type !== 'video' && ( -
- - {!!videoProgressPercent && ( -
-
+ + + + (['Enter', ' '].includes(e.key) && onClick())} + className={`group relative flex h-fit w-full max-w-md cursor-pointer flex-col gap-2 rounded-2xl transition-all duration-300 hover:-translate-y-2`} + > + {markAsCompleted && ( +
+ +
+ )} + {type === 'video' && ( +
+ +
+ )} + {type !== 'video' && ( +
+ + {!!videoProgressPercent && ( +
+
+
+ )} +
+ )} + {type === 'video' && ( +
+ +
+ )} +
+

+ {title} +

+ {bookmark !== undefined && contentId && ( + + )}
- )} -
- )} - {type === 'video' && ( -
- -
- )} -
-

- {title} -

- {bookmark !== undefined && contentId && ( - - )} -
-
+ +
+ { + Array.isArray(weeklyContentTitles) && weeklyContentTitles?.length > 0 && + {weeklyContentTitles?.map((title) =>

{title}

)} +
+ } +
+
); }; diff --git a/src/components/FolderView.tsx b/src/components/FolderView.tsx index e79887df3..c9f24c430 100644 --- a/src/components/FolderView.tsx +++ b/src/components/FolderView.tsx @@ -2,6 +2,7 @@ import { useRouter } from 'next/navigation'; import { ContentCard } from './ContentCard'; import { Bookmark } from '@prisma/client'; +import { CourseContentType } from '@/lib/utils'; export const FolderView = ({ courseContent, @@ -11,7 +12,7 @@ export const FolderView = ({ courseId: number; rest: string[]; courseContent: { - type: 'folder' | 'video' | 'notion'; + type: CourseContentType; title: string; image: string; id: number; @@ -20,6 +21,7 @@ export const FolderView = ({ videoFullDuration?: number; duration?: number; bookmark: Bookmark | null; + weeklyContentTitles?: string[]; }[]; }) => { const router = useRouter(); @@ -62,6 +64,7 @@ export const FolderView = ({ videoProgressPercent={videoProgressPercent} bookmark={content.bookmark} contentDuration={content.videoFullDuration} + weeklyContentTitles={content.weeklyContentTitles} /> ); })} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6c5b1e2c7..20036a9f7 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -366,3 +366,5 @@ export const getDisabledFeature = (feature: string): boolean => { .split(',') .includes(feature); }; + +export type CourseContentType = 'folder' | 'video' | 'notion' \ No newline at end of file From 6a49a643b2026325abf88c3db6bd9127dd40d865 Mon Sep 17 00:00:00 2001 From: Sargam Date: Wed, 9 Oct 2024 03:57:47 +0545 Subject: [PATCH 13/13] resolve all the problems related to build (#1463) * fix build * rm annoying spinner * small addons --- Dockerfile.prod | 10 +-- src/app/layout.tsx | 4 +- src/components/ContentCard.tsx | 1 + src/components/Navbar.tsx | 2 +- src/components/ThemeToggler.tsx | 4 +- src/components/comment/CommentInputForm.tsx | 10 +-- src/components/comment/CommentVoteForm.tsx | 2 +- .../stories/components/CourseCard.stories.ts | 83 ------------------- 8 files changed, 17 insertions(+), 99 deletions(-) delete mode 100644 storybook/stories/components/CourseCard.stories.ts diff --git a/Dockerfile.prod b/Dockerfile.prod index 3f3e5efc5..b952d8dd4 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -1,17 +1,17 @@ FROM node:20-alpine AS build WORKDIR /usr/src/app -ARG DATABASE_URL +ARG DATABASE_URL +ENV DATABASE_URL=${DATABASE_URL} COPY . . RUN npm install -g pnpm && \ pnpm install && \ pnpm add sharp && \ - pnpm run build && \ - DATABASE_URL=$DATABASE_URL pnpm dlx prisma generate - + pnpm run build +RUN DATABASE_URL=${DATABASE_URL} pnpm dlx prisma generate FROM node:20-alpine AS run @@ -25,7 +25,7 @@ COPY --from=build --chown=1001:1001 usr/src/app/.next/static ./.next/static COPY --from=build --chown=1001:1001 usr/src/app/public ./public ENV NODE_ENV production -ENV PORT 3000 +ENV PORT 3000 ENV HOSTNAME "0.0.0.0" CMD [ "node", "server.js" ] \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 976e9c458..4931b6ac4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -33,7 +33,9 @@ export default function RootLayout({ children }: { children: ReactNode }) { )} > - + {children} diff --git a/src/components/ContentCard.tsx b/src/components/ContentCard.tsx index 8c8a76c4b..edc944e40 100644 --- a/src/components/ContentCard.tsx +++ b/src/components/ContentCard.tsx @@ -6,6 +6,7 @@ import VideoThumbnail from './videothumbnail'; import CardComponent from './CardComponent'; import { motion } from 'framer-motion'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip'; +import React from 'react'; export const ContentCard = ({ title, diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b3841c077..d7b3fbe98 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -5,7 +5,7 @@ import { useSession } from 'next-auth/react'; import { usePathname, useRouter } from 'next/navigation'; import React, { useState, useCallback, useMemo } from 'react'; import Link from 'next/link'; -import { ArrowLeft, Menu, Search, X } from 'lucide-react'; +import { ArrowLeft, Menu, X } from 'lucide-react'; import { Button } from './ui/button'; import { AppbarAuth } from './AppbarAuth'; import ThemeToggler from './ThemeToggler'; diff --git a/src/components/ThemeToggler.tsx b/src/components/ThemeToggler.tsx index 88e613d88..473686f36 100644 --- a/src/components/ThemeToggler.tsx +++ b/src/components/ThemeToggler.tsx @@ -21,9 +21,7 @@ export default function ThemeToggler() { }; const toggleTheme = () => { - if (!document.startViewTransition) switchTheme(); - - document.startViewTransition(switchTheme); + switchTheme(); }; return ( diff --git a/src/components/comment/CommentInputForm.tsx b/src/components/comment/CommentInputForm.tsx index be991647e..753ce7525 100644 --- a/src/components/comment/CommentInputForm.tsx +++ b/src/components/comment/CommentInputForm.tsx @@ -42,11 +42,11 @@ const CommentInputForm = ({ setCommentText(''); }; - const isAllSpaces = (str: string): boolean => /^\s*$/.test(str); + const isAllSpaces = (str: string): boolean => (/^\s*$/).test(str); const isCommentValid = () => { return !isAllSpaces(commentText); - } + }; // Function to adjust the height of the textarea const adjustTextareaHeight = () => { @@ -58,11 +58,11 @@ const CommentInputForm = ({ useEffect(() => { if (!isCommentValid() || isLoading) { - setButtonDisabled(true) + setButtonDisabled(true); } else { - setButtonDisabled(false) + setButtonDisabled(false); } - }, [commentText]) + }, [commentText]); // Effect to handle the initial and dynamic height adjustment useEffect(() => { diff --git a/src/components/comment/CommentVoteForm.tsx b/src/components/comment/CommentVoteForm.tsx index 3ceedfcdd..98db02d55 100644 --- a/src/components/comment/CommentVoteForm.tsx +++ b/src/components/comment/CommentVoteForm.tsx @@ -64,7 +64,7 @@ const CommentVoteForm: React.FC = ({ } toast.promise( - execute({ voteType: newVoteType, commentId, currentPath }), + execute({ voteType: newVoteType, commentId, currentPath, slug: '' }), toastMessage, ); }; diff --git a/storybook/stories/components/CourseCard.stories.ts b/storybook/stories/components/CourseCard.stories.ts deleted file mode 100644 index 80f9a5301..000000000 --- a/storybook/stories/components/CourseCard.stories.ts +++ /dev/null @@ -1,83 +0,0 @@ -export const ButtonColor: Story = { - args: { - course: { - id: 1, - slug: 'course-slug', - appxCourseId: '1', - certIssued: false, - discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link - discordRoleId: 'discord-role-id', - title: 'Course Title', - description: 'Course Description', - imageUrl: - 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/test1.png', - totalVideos: 10, - totalVideosWatched: 5, - openToEveryone: true, - }, - onClick: () => {}, - }, -}; - -export const SmallRoundedCard: Story = { - args: { - course: { - id: 1, - slug: 'course-slug', - appxCourseId: '1', - discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link - certIssued: false, - discordRoleId: 'discord-role-id', - title: 'Course Title', - description: 'Course Description', - imageUrl: - 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/test1.png', - totalVideos: 10, - totalVideosWatched: 5, - openToEveryone: true, - }, - onClick: () => {}, - }, -}; - -export const MediumRoundedCard: Story = { - args: { - course: { - id: 1, - slug: 'course-slug', - appxCourseId: '1', - discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link - certIssued: false, - discordRoleId: 'discord-role-id', - title: 'Course Title', - description: 'Course Description', - imageUrl: - 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/test1.png', - totalVideos: 10, - totalVideosWatched: 5, - openToEveryone: true, - }, - onClick: () => {}, - }, -}; - -export const LargeRoundedCard: Story = { - args: { - course: { - id: 1, - slug: 'course-slug', - appxCourseId: '1', - discordOauthUrl: 'https://discord.com/invite/WAaXacK9bh', // Added Discord link - certIssued: false, - discordRoleId: 'discord-role-id', - title: 'Course Title', - description: 'Course Description', - imageUrl: - 'https://appx-recordings.s3.ap-south-1.amazonaws.com/drm/100x/images/test1.png', - totalVideos: 10, - totalVideosWatched: 5, - openToEveryone: true, - }, - onClick: () => {}, - }, -};