diff --git a/.github/ISSUE_TEMPLATE/add_link.yml b/.github/ISSUE_TEMPLATE/add_link.yml index 823981c40..15b810d82 100644 --- a/.github/ISSUE_TEMPLATE/add_link.yml +++ b/.github/ISSUE_TEMPLATE/add_link.yml @@ -24,6 +24,8 @@ body: - Artificial Intelligence - Internet of Things - Cloud Computing + - DevOps + - Competitive Programming - Youtube - Resources - Languages diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a95594ca..8f52ba086 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ Thank you for taking the time to contribute to our project. Please take a moment > For example, create a folder named `Resources` -- If you want to add a new subcategory, add it in [data.ts](https://github.com/rupali-codes/LinksHub/blob/main/database/data.ts) under the correct category, and provide the appropriate `url`. You can refer to the examples in the file. +- If you want to add a new subcategory, add it in [data.ts](https://github.com/rupali-codes/LinksHub/blob/main/database/data.ts) under the correct category, and provide the appropriate `URL`. You can refer to the examples in the file. - Make sure to export the newly created JSON file in the index file. @@ -61,7 +61,7 @@ Thank you for taking the time to contribute to our project. Please take a moment > For example, if you export the subcategory name as `onlineCodeEditors` in `database/index.ts`, add the same name to `CategoryDescriptions.ts` using the following style: - > subcategoryName: 'description of this subcategory' + > subcategory name: 'description of this subcategory' - You can check out similar examples [here](components/TopBar/CategoryDescriptions.ts). It's essential to add a description when submitting a pull request to add a subcategory; to merge it in the codebase. @@ -100,7 +100,7 @@ Thank you for taking the time to contribute to our project. Please take a moment - You can use [JSONLint](https://jsonlint.com/) to check the correctness of the JSON to avoid failing tests during pull requests. **NOTE** -When adding _YouTube_ channel link, please specify _the language_ of the channel they are using to teach for example English, Hindi, Spanish etc. In cases where the language is NOT specified, then just remove the `language` property. +When adding _YouTube_ channel links, please specify _the language_ of the channel they are using to teach for example English, Hindi, Spanish, etc. In cases where the language is not specified, then just remove the `language` property. > **⚠️Important** > @@ -152,33 +152,7 @@ When adding _YouTube_ channel link, please specify _the language_ of the channel - If you decide to close the issue, please leave a brief comment describing why(e.g., I'm busy with other obligations.) before you do. - **Note:** If the Pull Request associated with the issue gets merged and the issue still remains open, it's **your** responsibility to close the issue. -## Commits - -- Please keep your commit messages short and clear. -- Use the `type: subject` format for writing your commit messages. `type` could be one of the following: - - `feat`: use this if you're adding any new feature - - `fix`: use this if you're fixing anything in the codebase - - `chore`: use this when you're adding new links/resources AND when making any small changes - (ex. chore: add _resource_name_ in _subcategory_name_ _category_name_ ) - If you need more tips, check out [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - -## Making Pull Requests - -1. When you submit a pull request, several tests are automatically run - as GitHub Actions. If any of these tests fail, it is your responsibility to try and resolve the underlying issue(s). If you don't know how to resolve the underlying issue(s), you can ask for help. - -2. Each pull request should contain a single logical change or related set of changes that make sense to submit together. If a pull request becomes too large or contains too many unrelated changes, it becomes too difficult to review. In such cases, the reviewer has the right to close your pull request and ask that you submit a separate pull request for each logical set of changes that belong together. - -3. Link the issue you have resolved in the Pull Request Template (e.g. Closes/Fixes #99). -4. Use [Conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/) for your changes. -5. Do not re-open a pull request that a reviewer has closed. - - Make sure to tick the "Allow edits from maintainers" box. This allows us to directly make minor edits / refactors and saves a lot of time. - > **Note** - > If your pull request has merge conflicts with the `main` branch (GitHub checks for this automatically and notifies you), you are responsible for resolving them. You can do this by merging the `main` branch into your branch (`git pull upstream main`), and then pushing the updated branch to your fork (`git push`). If you need more tips, check out [Resolving a merge conflict on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-on-github). - ---- - -### Commit Message Guidelines using Commitlint +## Commits Message Guidelines We follow a standardized commit message format using Commitlint to ensure consistency and clarity in our commit history. Each commit message should adhere to the following guidelines: @@ -198,26 +172,24 @@ We follow a standardized commit message format using Commitlint to ensure consis 4. **Issue reference** (Optional): Include the issue number associated with the commit (e.g., `#123`). -### Examples: - -#### Valid Commit Messages: +#### Examples of Valid Commit Messages: - `feat: Add user authentication feature` -- `fix(auth): Resolve login page redirect issue` -- `docs: Update installation instructions` -- `style: Format code according to project guidelines` +- `fix(auth): Resolve login page redirect issue +- `docs: Update installation instructions +- `style: Format code according to project guidelines - `refactor(navbar): Improve responsiveness` - `test: Add unit tests for API endpoints` -- `chore: Update dependencies to latest versions` +- `chore: Update dependencies to latest versions - `fix: Handle edge case in data processing (#456)` -#### Invalid Commit Messages: +#### Examples of Invalid Commit Messages: - `Added new stuff` - `Fixed a bug` - `Updated code` - `auth feature update` -- `chore: fixed some stuff` +- `chore: fixed some stuff ### Commit Example with Commitlint: @@ -226,6 +198,22 @@ git commit -m "feat(auth): Implement user signup process (#789)" ```
+## Making Pull Requests + +1. When you submit a pull request, several tests are automatically run + as GitHub Actions. If any of these tests fail, it is your responsibility to try and resolve the underlying issue(s). If you don't know how to resolve the underlying issue(s), you can ask for help. + +2. Each pull request should contain a single logical change or related set of changes that make sense to submit together. If a pull request becomes too large or contains too many unrelated changes, it becomes too difficult to review. In such cases, the reviewer has the right to close your pull request and ask that you submit a separate pull request for each logical set of changes that belong together. + +3. Link the issue you have resolved in the Pull Request Template (e.g. Closes/Fixes #99). +4. Use [Conventional commit messages](https://www.conventionalcommits.org/en/v1.0.0/) for your changes. +5. Do not re-open a pull request that a reviewer has closed. + - Make sure to tick the "Allow edits from maintainers" box. This allows us to directly make minor edits / refactors and saves a lot of time. + > **Note** + > If your pull request has merge conflicts with the `main` branch (GitHub checks for this automatically and notifies you), you are responsible for resolving them. You can do this by merging the `main` branch into your branch (`git pull upstream main`), and then pushing the updated branch to your fork (`git push`). If you need more tips, check out [Resolving a merge conflict on GitHub](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-on-github). + +--- + ## Remarks ✅ - If something is missing here, or you feel something is not well described, either create a PR, [raise an issue](https://github.com/rupali-codes/LinksHub/issues), or [do a code review of the person’s PR](https://www.freecodecamp.org/news/code-review-tips/) (ensure that your review conforms to the [Code of Conduct](https://github.com/CBID2/LinksHub-my-version-/blob/main/CODE_OF_CONDUCT.md)) diff --git a/README.md b/README.md index 375656ebc..614dc4555 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ We recognize that there's a wealth of information available, but often, it's a m - [Socials 📱](#socials-) - [Getting Started 👩‍💻](#getting-started-) - [Let's jump right in🌟](#lets-jump-right-in) - - [_Want to add your favourite links to the Hub? make sure to follow CONTRIBUTING guidelines._](#want-to-add-your-favourite-links-to-the-hub-make-sure-to-follow-contributing-guidelines) + - [_Want to add your favorite links to the Hub? make sure to follow CONTRIBUTING guidelines._](#want-to-add-your-favourite-links-to-the-hub-make-sure-to-follow-contributing-guidelines) - [Want to add or update the descriptions of subcategories?](#want-to-add-or-update-the-descriptions-of-subcategories) - [Building with Gitpod 💣](#building-with-gitpod-) - [Top 50 Contributors ✨](#top-50-contributors-) @@ -106,10 +106,10 @@ You can see the live demo at: https://linkshub.vercel.app > ``` > 2. Start the docker container with: > ```bash -> docker compose up +> docker-compose up > ``` > 3. Now start adding your changes. -> **Note:** You don't need to restart the container again and again after starting it once, because the changes you make will reflect into the container instantly. +> **Note:** You don't need to restart the container again and again after starting it once, because the changes you make will reflect in the container instantly. 7. Make your changes before staging them. @@ -135,7 +135,7 @@ You can see the live demo at: https://linkshub.vercel.app > Click _compare across forks_ if you don't see your branch -#### _Want to add your favourite links to the Hub? make sure to follow [CONTRIBUTING guidelines](https://github.com/rupali-codes/linkshub/blob/main/CONTRIBUTING.md)._ +#### _Want to add your favorite links to the Hub? make sure to follow [CONTRIBUTING guidelines](https://github.com/rupali-codes/linkshub/blob/main/CONTRIBUTING.md)._ #### Want to add or update the descriptions of subcategories? Make sure to follow [CONTRIBUTING guidelines](./CONTRIBUTING.md#Adding-and-Updating-Category-Description-). @@ -146,7 +146,7 @@ Make sure to follow [CONTRIBUTING guidelines](./CONTRIBUTING.md#Adding-and-Updat ## Building with Gitpod 💣 -By using [Gitpod.io](https://www.gitpod.io), all the necessary dependencies will be installed +By using [Gitpod. io](https://www.gitpod.io), all the necessary dependencies will be installed and the website will be built in one single click. No extra setup is required. [![Gitpod Ready-to-Code](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/rupali-codes/LinksHub) @@ -175,6 +175,6 @@ LinksHub is licensed under the terms of the MIT License. check out [LICENSE](htt ## Support ⭐ -_We would love to have you, feel free to open issues and pull requests and Don't forget to leave a star⭐_ +_We would love to have you, feel free to open issues and pull requests, and Don't forget to leave a star⭐_ _We would be thrilled to have you contribute to LinksHub! Your support, whether through opening issues, submitting pull requests, or even just leaving a star, means a lot to us. Together, we can continue to improve and expand this resource hub for developers worldwide._ diff --git a/SECURITY.md b/SECURITY.md index bbad04c56..69a0d5b42 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,7 +10,7 @@ The security policy covers the codebase and documentation of the open source pro ## Vulnerability Disclosure Process -The project will provide a dedicated email address (rupali7487@gmail.com) for submitting vulnerability reports related to the [Linkshub](https://linkshub.vercel.app/) website or any of the linked websites. Vulnerability reports will be reviewed and triaged by the project's maintainers. The owner will aim to respond to vulnerability reports within 72 hours, and will provide regular updates on the status of the vulnerability and any remediation efforts. +The project will provide a dedicated email address (rupali7487@gmail.com) for submitting vulnerability reports related to the [Linkshub](https://linkshub.vercel.app/) website or any of the linked websites. Vulnerability reports will be reviewed and triaged by the project's maintainers. The owner will aim to respond to vulnerability reports within 72 hours and will provide regular updates on the status of the vulnerability and any remediation efforts. ## Roles and Responsibilities @@ -22,7 +22,7 @@ LinksHub will aim to resolve critical vulnerabilities within 30 days and non-cri ## Secure Coding Practices -LinksHub will provide guidance on secure coding practices for contributors, including guidelines for input validation, authentication, authorization, and data protection. +LinksHub will guide secure coding practices for contributors, including guidelines for input validation, authentication, authorization, and data protection. ## Regular Review and Update @@ -30,7 +30,7 @@ The security policy will be regularly reviewed and updated to ensure that it rem ## Disclosure Policy -LinksHub will follow a coordinated disclosure policy, which means that vulnerabilities will be disclosed publicly only after they have been remediated. The project may work with external website owners to coordinate disclosure of vulnerabilities that affect their websites. +LinksHub will follow a coordinated disclosure policy, which means that vulnerabilities will be disclosed publicly only after they have been remediated. The project may work with external website owners to coordinate the disclosure of vulnerabilities that affect their websites. ## Legal Disclaimer @@ -40,4 +40,4 @@ The security policy includes a legal disclaimer that limits the liability of the If you have any questions or concerns about the security policy or any security vulnerabilities in the project, please contact us at _linkshub.opensource@gmail.com_. -By implementing this security policy, we aim to ensure that vulnerabilities are addressed in a timely manner, and that users and contributors can use [Linkshub](https://linkshub.vercel.app/) and its linked sources safely and securely. +By implementing this security policy, we aim to ensure that vulnerabilities are addressed promptly and that users and contributors can use [Linkshub](https://linkshub.vercel.app/) and its linked sources safely and securely. diff --git a/components/CopyToClipboard/CopyToClipboard.tsx b/components/CopyToClipboard/CopyToClipboard.tsx index e3f2a8a5f..8436b4002 100644 --- a/components/CopyToClipboard/CopyToClipboard.tsx +++ b/components/CopyToClipboard/CopyToClipboard.tsx @@ -1,33 +1,56 @@ -import useCopyToClipboard from 'hooks/useCopyToClipboard' -import React from 'react' -import { FaRegCopy } from 'react-icons/fa' -import { Tooltip } from 'react-tooltip' +import useCopyToClipboard from "hooks/useCopyToClipboard"; +import React from "react"; +import { FaRegCopy, FaCheckSquare } from "react-icons/fa"; +import { Tooltip } from "react-tooltip"; type CopyToClipboardProps = { - url: string -} + url: string; +}; export const CopyToClipboard = ({ url }: CopyToClipboardProps): JSX.Element => { - const [copyToClipboard, { success }] = useCopyToClipboard() + const [copyToClipboard, { success }] = useCopyToClipboard(); function handleCopy(e: React.MouseEvent) { - e.stopPropagation() - copyToClipboard(url) + e.stopPropagation(); + copyToClipboard(url); } return ( -
-
- - +
- ) -} + ); +} \ No newline at end of file diff --git a/components/ErrorMessage/index.tsx b/components/ErrorMessage/index.tsx new file mode 100644 index 000000000..be3b7303f --- /dev/null +++ b/components/ErrorMessage/index.tsx @@ -0,0 +1,14 @@ +interface ErrorMessageProps { + children: React.ReactNode + className?: string | undefined +} + +export const ErrorMessage: React.FC = ({ + children, + className, +}) => { + const defaultClasses = 'text-red-500 mt-2' + const classes = defaultClasses + ' ' + (className ?? '') + + return

{children}

+} diff --git a/components/Header/Header.tsx b/components/Header/Header.tsx index 61c8d57dc..244e14f18 100644 --- a/components/Header/Header.tsx +++ b/components/Header/Header.tsx @@ -14,7 +14,7 @@ export const Header: FC = () => { return (
-
+
diff --git a/components/Searchbar/Searchbar.tsx b/components/Searchbar/Searchbar.tsx index 7b53d4849..0dfd7b0d5 100644 --- a/components/Searchbar/Searchbar.tsx +++ b/components/Searchbar/Searchbar.tsx @@ -1,71 +1,80 @@ -import React, { useState, useRef } from 'react' +import { useRef, useEffect } from 'react' + import SearchIcon from 'assets/icons/SearchIcon' -import { useRouter } from 'next/router' +import { SearchbarSuggestions } from './SearchbarSuggestions' +import { ErrorMessage } from 'components/ErrorMessage' + import { subcategoryArray } from '../../types' +import { SearchbarAction } from './SearchbarReducer' +import { useRouter } from 'next/router' interface SearchbarProps { - setSearch: (search: string) => void + dispatchSearch: (action: SearchbarAction) => void + searchQuery: string + showSuggestions: boolean + searchQueryIsValid: boolean } -export const Searchbar: React.FC = ({ setSearch }) => { +const searchOptions = subcategoryArray +const SEARCH_ERROR_MSG = 'Please enter a valid search query' + +export const Searchbar: React.FC = ({ + dispatchSearch, + searchQuery, + showSuggestions, + searchQueryIsValid, +}) => { + const formRef = useRef(null) const router = useRouter() - const query = router.query.query - const [searchQuery, setSearchQuery] = useState((query as string) ?? '') - const [errorMessage, setErrorMessage] = useState('') - const [suggestions, setSuggestions] = useState([]) - const dropdownRef = useRef(null) + const suggestions = getFilteredSuggestions(searchQuery) const handleSearchChange = (e: React.ChangeEvent) => { - const value = e.target.value - setSearchQuery(value) - - const trimmedValue = value.trim().toLowerCase() - if (trimmedValue === '') { - setErrorMessage('') - setSuggestions([]) - setSearch('') - } else { - const filteredSuggestions = subcategoryArray.filter((option) => - option.toLowerCase().includes(trimmedValue) - ) - setSuggestions(filteredSuggestions) - } + dispatchSearch({ + type: 'search_query_change', + searchQuery: e.target.value, + }) } - const handleSuggestionClick = (suggestion: string) => { - setSearchQuery(suggestion) - setSearch(suggestion) - setSuggestions([]) + const handleSuggestionClick = (searchQuery: string) => { + dispatchSearch({ type: 'suggestion_click', searchQuery }) + router.push({ + pathname: '/search', + query: { + query: searchQuery, + }, + }) } const handleSubmit = (e: React.FormEvent) => { e.preventDefault() - if (searchQuery.trim() === '') { - setErrorMessage('Please enter a search query') - } else { - setErrorMessage('') - setSearch(searchQuery) + + dispatchSearch({ type: 'submit' }) + if (searchQuery.trim() !== '') { + router.push({ + pathname: '/search', + query: { + query: searchQuery, + }, + }) } } - const handleClickOutsideDropdown = (e: MouseEvent) => { - if ( - dropdownRef.current && - !dropdownRef.current.contains(e.target as Node) - ) { - setSuggestions([]) + useEffect(() => { + const handleClickOutsideDropdown = (e: MouseEvent) => { + if ((formRef.current as HTMLFormElement).contains(e.target as Node)) + return + dispatchSearch({ type: 'close_suggestions' }) } - } - React.useEffect(() => { document.addEventListener('mousedown', handleClickOutsideDropdown) + return () => { document.removeEventListener('mousedown', handleClickOutsideDropdown) } - }, []) + }, [dispatchSearch]) return ( -
+
- {suggestions.length > 0 && ( -
    - {suggestions.map((suggestion) => ( -
  • handleSuggestionClick(suggestion)} - > - {suggestion.split('-').join(' ')} -
  • - ))} -
+ {suggestions.length > 0 && showSuggestions && ( + )}
- {errorMessage &&

{errorMessage}

} + {!searchQueryIsValid && {SEARCH_ERROR_MSG}}
) } + +const getFilteredSuggestions = (query: string) => { + const normalisedQuery = query.trim().toLowerCase() + if (normalisedQuery.length === 0) { + return [] + } + + const suggestions = new Set([]) + searchOptions.forEach((option) => { + const normalisedOption = option.toLowerCase() + if (normalisedOption.includes(normalisedQuery)) { + suggestions.add(normalisedOption) + } + }) + + return Array.from(suggestions) +} diff --git a/components/Searchbar/SearchbarReducer.ts b/components/Searchbar/SearchbarReducer.ts new file mode 100644 index 000000000..01fd21b30 --- /dev/null +++ b/components/Searchbar/SearchbarReducer.ts @@ -0,0 +1,69 @@ +export interface SearchbarState { + searchQuery: string + categoryQuery: string + searchQueryIsValid: boolean + showSuggestions: boolean +} + +export interface SearchbarAction extends Partial { + type: string +} + +export const initialState: SearchbarState = { + searchQuery: '', + categoryQuery: '', + searchQueryIsValid: true, + showSuggestions: false, +} + +export const searchbarReducer: ( + state: SearchbarState, + action: SearchbarAction +) => SearchbarState = (state, action) => { + switch (action.type) { + case 'search_query_change': { + const query = action.searchQuery as string + const normalisedQuery = query.trim() + let categoryQuery = state.categoryQuery + if (normalisedQuery === '') { + categoryQuery = '' + } + + return { + searchQuery: query, + categoryQuery, + searchQueryIsValid: true, + showSuggestions: true, + } + } + case 'suggestion_click': { + return { + showSuggestions: false, + categoryQuery: action.searchQuery as string, + searchQuery: action.searchQuery as string, + searchQueryIsValid: true, + } + } + case 'submit': { + const searchQueryIsValid = (state.searchQuery).trim() !== '' + + return { + searchQuery: state.searchQuery, + categoryQuery: searchQueryIsValid + ? (state.searchQuery) + : state.categoryQuery, + showSuggestions: false, + searchQueryIsValid, + } + } + case 'close_suggestions': { + return { + ...state, + showSuggestions: false, + } + } + default: { + return state + } + } +} diff --git a/components/Searchbar/SearchbarSuggestions.tsx b/components/Searchbar/SearchbarSuggestions.tsx new file mode 100644 index 000000000..5093905c7 --- /dev/null +++ b/components/Searchbar/SearchbarSuggestions.tsx @@ -0,0 +1,23 @@ +interface SuggestionsProps { + suggestions: string[] + onSuggestionClick: (suggestion: string) => void +} + +export const SearchbarSuggestions: React.FC = ({ + suggestions, + onSuggestionClick, +}) => { + return ( +
    + {suggestions.map((suggestion) => ( +
  • onSuggestionClick(suggestion)} + > + {suggestion.replace('-', ' ')} +
  • + ))} +
+ ) +} diff --git a/components/Share/Share.tsx b/components/Share/Share.tsx index fdc17cb1c..320977ce1 100644 --- a/components/Share/Share.tsx +++ b/components/Share/Share.tsx @@ -1,62 +1,64 @@ -import React from 'react' -import { FiShare2 } from 'react-icons/fi' -import { Tooltip } from 'react-tooltip' +import React from "react"; +import { FiShare2 } from "react-icons/fi"; +import { Tooltip } from "react-tooltip"; type ShareProps = { - url: string - title: string -} + url: string; + title: string; +}; export const Share: React.FC = ({ url, title }) => { const showShareOptions = false - async function handleShare() { if (navigator.share) { try { await navigator.share({ title: title, url: url, - }) + }); } catch (error) { - console.error('Error sharing:', error) + console.error("Error sharing:", error); } } else { - console.log('Web Share API not supported on this browser.') + console.log("Web Share API not supported on this browser."); // Fallback behavior when Web Share API is not supported (e.g., open a new tab with the URL) - window.open(url, '_blank') + window.open(url, "_blank"); } } return (
- + backgroundColor: "#8b5cf6", + fontSize: "13px", + paddingLeft: "6px", + paddingRight: "6px", + paddingTop: "2px", + paddingBottom: "2px", + }} + /> + {showShareOptions && (

= ({ url, title }) => {

)}
- ) -} + ); +}; -export default Share +export default Share; \ No newline at end of file diff --git a/components/SideNavbar/SideNavbarBody.tsx b/components/SideNavbar/SideNavbarBody.tsx index 7a2360035..f995151e4 100644 --- a/components/SideNavbar/SideNavbarBody.tsx +++ b/components/SideNavbar/SideNavbarBody.tsx @@ -1,13 +1,17 @@ -import { FC } from 'react' +import { FC, memo } from 'react' import { Searchbar } from 'components/Searchbar/Searchbar' -import useSidebarSearch from 'hooks/useSidebarSearch' import classNames from 'classnames' import { useTheme } from 'next-themes' + import { SideNavbarCategoryList } from './SideNavbarCategoryList' +import { useSearchReducer } from 'hooks/useSearchReducer' + +const MemoizedSideNavbarCategoryList = memo(SideNavbarCategoryList) + export const SideNavbarBody: FC = () => { const { theme } = useTheme() - const { setSearch, searchResults, debouncedSearch } = useSidebarSearch() + const [searchState, dispatchSearch] = useSearchReducer() return (
{ )} >
- -
- -
- 0} - openByDefault={''} - /> +
+
) } diff --git a/components/SideNavbar/SideNavbarCategory.tsx b/components/SideNavbar/SideNavbarCategory.tsx index 84931ccb9..f4caee83e 100644 --- a/components/SideNavbar/SideNavbarCategory.tsx +++ b/components/SideNavbar/SideNavbarCategory.tsx @@ -1,27 +1,27 @@ -import { FC } from 'react' +import { FC, useState } from 'react' import { FaAngleDown } from 'react-icons/fa' import { SideNavbarElement } from './SideNavbarElement' -import type { ISidebar, Category } from '../../types' +import type { ISidebar } from '../../types' export const SideNavbarCategory: FC<{ - item: ISidebar - handleToggle: (category: Category, isOpen: boolean) => void - isOpen: boolean -}> = (props) => { - const { item, isOpen } = props + categoryData: ISidebar + expand: boolean +}> = ({ categoryData, expand }) => { + const [isOpen, setIsOpen] = useState(expand) - const handleToggle = () => { - props.handleToggle(item.category, isOpen) - } - - const subcategoryList = item.subcategory + const { category, subcategory } = categoryData + const sortedSubcategoryList = subcategory .sort((a, b) => (a.name.toUpperCase() < b.name.toUpperCase() ? -1 : 1)) - .map((list, i) => ( + .map((subcategoryData, i) => (
  • - +
  • )) + const handleToggle = () => { + setIsOpen(!isOpen) + } + return (
  • diff --git a/components/SideNavbar/SideNavbarCategoryList.tsx b/components/SideNavbar/SideNavbarCategoryList.tsx index 62f49addd..670196a58 100644 --- a/components/SideNavbar/SideNavbarCategoryList.tsx +++ b/components/SideNavbar/SideNavbarCategoryList.tsx @@ -1,94 +1,64 @@ -import { FC, useEffect, useState } from 'react' -import type { ISidebar, Category } from '../../types' +import React, { FC } from 'react' +import type { SubCategories } from '../../types' +import { sidebarData } from 'database/data' import { SideNavbarCategory } from './SideNavbarCategory' export const SideNavbarCategoryList: FC<{ - items: ISidebar[] - openByDefault: string - isSearching: boolean -}> = (props) => { - const { items, openByDefault, isSearching } = props + query: string +}> = ({ query }) => { + const categoriesList = getFilteredCategoryList(query) - const initialOpenState = items.reduce( - (acc, item) => ({ - ...acc, - [item.category]: openByDefault === item.category || false, - }), - {} - ) - - const [isItemsOpen, setIsItemsOpen] = - useState>(initialOpenState) - const [statePriorToSearch, setStatePriorToSearch] = - useState>(initialOpenState) + if (categoriesList.length === 0) { + return ( +
    + No Links Found +
    + ) + } - // console.log(isItemsOpen, isSearching) + return ( +
      + {/* Reset the open/close states of categories. This makes sure that + changes to the toggle state don't reflect when a new query is submitted */} + + {categoriesList.map((categoryData) => ( + 0} + /> + ))} + +
    + ) +} - useEffect(() => { - setIsItemsOpen((prev) => ({ - ...prev, - ...items.reduce( - (acc, item) => ({ - ...acc, - [item.category]: isSearching || prev[item.category], - }), - {} - ), - })) +const getFilteredCategoryList = (query: string) => { + const filteredResults = sidebarData.filter((sidebarItem) => + sidebarItem.subcategory.some((subCategory) => + matchSearch(subCategory, query) + ) + ) + const mappedResults = filteredResults.map((sidebarItem) => ({ + ...sidebarItem, + subcategory: sidebarItem.subcategory.filter((subcategory) => + matchSearch(subcategory, query) + ), + })) - if (!isSearching) { - setStatePriorToSearch((prev) => ({ - ...prev, - ...items.reduce( - (acc, item) => ({ - ...acc, - [item.category]: prev[item.category], - }), - {} - ), - })) - } - }, [isSearching, items]) + return mappedResults +} - /** - * @param category the category to toggle - * @param isOpen the current open state of the category - * @returns void - * @description toggle the open state of the category and closes all other categories - */ - const handleToggle = (category: Category, isOpen: boolean) => { - setIsItemsOpen((prev) => ({ ...prev, [category]: !isOpen })) +const matchSearch = (item: SubCategories, query: string) => { + const itemName = item.name.toLowerCase() + const matchingResources = item.resources.filter( + (resource: { name: string }) => + resource.name.toLowerCase().includes(query.toLowerCase()) + ) - if (!isSearching) { - /** - * @description save the state of categories that are opened when not searching, to restore them when the search is closed - */ - setStatePriorToSearch((prev) => ({ ...prev, [category]: !isOpen })) - } - } return ( -
      - {items.length !== 0 ? ( - items.map((item, index) => { - return ( - - ) - }) - ) : ( -
      - No Links Found -
      - )} -
    + query === '' || + itemName.includes(query.toLowerCase()) || + matchingResources.length > 0 ) } diff --git a/components/ThemeToggler/themeToggler.tsx b/components/ThemeToggler/themeToggler.tsx index 850abee07..21a8cbf66 100644 --- a/components/ThemeToggler/themeToggler.tsx +++ b/components/ThemeToggler/themeToggler.tsx @@ -1,21 +1,22 @@ -import { useState, useEffect } from 'react' -import { useTheme } from 'next-themes' -import { HiSun, HiMoon } from 'react-icons/hi' +import { useState, useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import { HiSun, HiMoon } from 'react-icons/hi'; +import { Helmet, HelmetProvider } from 'react-helmet-async'; export function ThemeToggler() { - const { resolvedTheme, setTheme } = useTheme() - const [mounted, setMounted] = useState(false) + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); useEffect(() => { - setMounted(true) - }, []) + setMounted(true); + }, []); if (!mounted) { - return null + return null; } const handleThemeToggle = () => { - setTheme(resolvedTheme === 'dark' ? 'light' : 'dark') + setTheme(resolvedTheme === 'dark' ? 'light' : 'dark'); } const iconProps = { @@ -23,23 +24,36 @@ export function ThemeToggler() { size: '1.5rem', } + const themeColor = resolvedTheme === 'dark' ? '#0F172A' : '#F5F3FF'; + return ( - - ) + + + + ); } diff --git a/database/DevOps/docker.json b/database/DevOps/docker.json index 7ab86f000..fa0c4f29d 100644 --- a/database/DevOps/docker.json +++ b/database/DevOps/docker.json @@ -5,5 +5,12 @@ "url": "https://www.mygreatlearning.com/blog/docker-tutorial/", "category": "DevOps", "subcategory": "docker" + }, + { + "name":"Docker", + "description":"In this free video series, you will learn the basics of Docker and containerization..", + "url":"https://kodekloud.com/lessons/introduction-9/", + "category":"DevOps", + "subcategory":"docker" } ] diff --git a/database/backend/database.json b/database/backend/database.json index 4f1a03e01..0483537e5 100644 --- a/database/backend/database.json +++ b/database/backend/database.json @@ -61,5 +61,12 @@ "url": "https://docs.aws.amazon.com/amplify/", "category": "backend", "subcategory": "database" + }, + { + "name": "GraphQL", + "description": "GraphQL provides a comprehensive description of the data in your API, which gives clients the power to ask for exactly what they need and nothing more and make it easier to evolve APIs over time. It also enables powerful developer tools.", + "url": "https://graphql.org/", + "category": "backend", + "subcategory": "database" } ] \ No newline at end of file diff --git a/database/cloud_computing_platforms/oracle.json b/database/cloud_computing_platforms/oracle.json index 8fc23f062..4ae24f00e 100644 --- a/database/cloud_computing_platforms/oracle.json +++ b/database/cloud_computing_platforms/oracle.json @@ -5,5 +5,12 @@ "url": "https://docs.oracle.com/en/cloud/get-started/index.html", "category": "cloud computing", "subcategory": "oracle" + }, + { + "name": "OCI certifications", + "description": "Accelerate your cloud journey along with free certification from Oracle", + "url": "https://education.oracle.com/home", + "category": "cloud computing", + "subcategory": "oracle" } ] diff --git a/database/competitive_programming/platforms.json b/database/competitive_programming/platforms.json index 7a05d241c..52b0dda82 100644 --- a/database/competitive_programming/platforms.json +++ b/database/competitive_programming/platforms.json @@ -12,5 +12,33 @@ "url": "https://www.hackerrank.com/", "category": "competitive_programming", "subcategory": "platforms" + }, + { + "name": "Codeforces", + "description": "Codeforces is a website for practicing competitive programming", + "url": "https://codeforces.com/", + "category": "competitive_programming", + "subcategory": "platforms" + }, + { + "name": "SPOJ", + "description": "SPOJ is an online judge system with over 315,000 registered users and over 20,000 problems.", + "url": "https://www.spoj.com/", + "category": "competitive_programming", + "subcategory": "platforms" + }, + { + "name": "Geek for Geeks", + "description": "This website is the best place to practice programming problems, review company interview questions, and improve your coding skills", + "url": "https://practice.geeksforgeeks.org/", + "category": "competitive_programming", + "subcategory": "platforms" + }, + { + "name": "Coding Ninjas", + "description": "Want to participate in coding wars? Compete with other coders worldwide by participating in the contests. You also earn points for your achievements.", + "url": "https://www.codingninjas.com/studio/home", + "category": "competitive_programming", + "subcategory": "platforms" } ] diff --git a/database/data.ts b/database/data.ts index b3e9495a7..a947c1aa5 100644 --- a/database/data.ts +++ b/database/data.ts @@ -11,6 +11,8 @@ export const sidebarData: ISidebar[] = [ category: 'frontend', subcategory: [ { name: 'react js', url: '/react', resources: DB.react }, + { name: 'next js', url: '/next', resources: DB.next }, + { name: 'three js', url: '/three', resources: DB.three }, { name: 'images', url: '/images', resources: DB.images }, { name: 'fonts', url: '/fonts', resources: DB.fonts }, { name: 'colors', url: '/colors', resources: DB.colors }, @@ -261,7 +263,7 @@ export const sidebarData: ISidebar[] = [ ], }, { - category: 'competitive_programming', + category: 'cp - competitive pro', subcategory: [ { name: 'platforms', url: '/platforms', resources: DB.platforms }, ], diff --git a/database/frontend/fonts.json b/database/frontend/fonts.json index d86a420f0..20db0dd56 100644 --- a/database/frontend/fonts.json +++ b/database/frontend/fonts.json @@ -68,5 +68,12 @@ "url": "https://www.dafont.com/", "category": "frontend", "subcategory": "fonts" + }, + { + "name": "Adobe Fonts", + "description": "Adobe Fonts partners with the world's leading type foundries to bring thousands of beautiful fonts to designers every day.", + "url": "https://fonts.adobe.com/", + "category": "frontend", + "subcategory": "fonts" } -] \ No newline at end of file +] diff --git a/database/frontend/icons.json b/database/frontend/icons.json index c31750827..01055b268 100644 --- a/database/frontend/icons.json +++ b/database/frontend/icons.json @@ -194,5 +194,12 @@ "url": "https://icon-icons.com/", "category": "frontend", "subcategory": "icons" + }, + { + "name": "The Noun Project", + "description": "The Noun Project offers the world's most diverse collection of free icons and vector images.", + "url": "https://thenounproject.com/", + "category": "frontend", + "subcategory": "icons" } -] \ No newline at end of file +] diff --git a/database/frontend/images.json b/database/frontend/images.json index 70646f435..a0946791d 100644 --- a/database/frontend/images.json +++ b/database/frontend/images.json @@ -152,5 +152,12 @@ "url": "https://stock.adobe.com/in/", "category": "frontend", "subcategory": "images" + }, + { + "name": "Vector Stock", + "description": "VectorStock is the world’s premiere vector-only image marketplace", + "url": "https://www.vectorstock.com/", + "category": "frontend", + "subcategory": "images" } ] diff --git a/database/frontend/next.json b/database/frontend/next.json new file mode 100644 index 000000000..03069b410 --- /dev/null +++ b/database/frontend/next.json @@ -0,0 +1,9 @@ +[ + { + "name": "NextJS", + "description": "Used by some of the world's largest companies, Next.js enables you to create full-stack Web applications by extending the latest React features and integrating powerful Rust-based JavaScript tooling for the fastest builds.", + "url": "https://nextjs.org/", + "category": "frontend", + "subcategory": "next-js" + } + ] diff --git a/database/frontend/online-code-editors.json b/database/frontend/online-code-editors.json index 2f636d6e8..52bfda558 100644 --- a/database/frontend/online-code-editors.json +++ b/database/frontend/online-code-editors.json @@ -124,5 +124,12 @@ "url": "https://ideone.com/", "category": "frontend", "subcategory": "online-code-editors" + }, + { + "name": "Jdoodle", + "description": " It is a free online compiler and code editor to save, run and share code anytime", + "url": "https://www.jdoodle.com/", + "category": "frontend", + "subcategory": "online-code-editors" } ] diff --git a/database/frontend/three.json b/database/frontend/three.json new file mode 100644 index 000000000..4e906afc8 --- /dev/null +++ b/database/frontend/three.json @@ -0,0 +1,9 @@ +[ + { + "name": "ThreeJS", + "description": "Three.js is the world's most popular JavaScript framework for displaying 3D content on the web. With three.js, you no longer need a fancy gaming PC console or download a special application to display photorealistic 3D graphics. All you need is a smartphone and a web browser.", + "url": "https://discoverthreejs.com/", + "category": "frontend", + "subcategory": "three-js" + } + ] diff --git a/database/index.ts b/database/index.ts index b44becb29..23c2cf63f 100644 --- a/database/index.ts +++ b/database/index.ts @@ -11,6 +11,8 @@ export { default as onlineCodeEditors } from './frontend/online-code-editors.jso export { default as themesTemplates } from './frontend/themes-templates.json' export { default as uiGenerators } from './frontend/ui-generators.json' export { default as react } from './frontend/react.json' +export { default as next } from './frontend/next.json' +export { default as three } from './frontend/three.json' export { default as videos } from './frontend/videos.json' // backend export { default as authentication } from './backend/authentication.json' diff --git a/database/other/communities.json b/database/other/communities.json index 81c7d8391..4fd7a2d0e 100644 --- a/database/other/communities.json +++ b/database/other/communities.json @@ -47,5 +47,12 @@ "url": "https://aws.amazon.com/education/awseducate/", "category": "other", "subcategory": "communities" + }, + { + "name": "VMware vExpert Program", + "description": "The VMware vExpert Program is VMware’s global evangelism and advocacy program initiative that awards individuals who have made significant community contributions to the VMware ecosystem.", + "url": "https://vexpert.vmware.com/", + "category": "other", + "subcategory": "communities" } ] \ No newline at end of file diff --git a/database/other/events.json b/database/other/events.json index faa9a9cb5..6bd7dbe2b 100644 --- a/database/other/events.json +++ b/database/other/events.json @@ -20,5 +20,12 @@ "url": "https://medium.com/@jonathanallengrant/the-7-best-websites-to-find-a-hackathon-the-hacklife-guide-3e420f2a565b", "category": "other", "subcategory": "events" + }, + { + "name": "Best Open Source Programs For Students to Participate", + "description": "Contributing to open source is a really great way to get real-world software development and other domain experience from the comfort of your home even if you’re a beginner and don’t have a job in the industry. In this article, you’ll learn about the various programs that are available for university students and even for working professionals to get involved in open source. So let’s get started!", + "url": "https://www.geeksforgeeks.org/best-open-source-programs-for-students-to-participate/", + "category": "other", + "subcategory": "events" } ] diff --git a/database/resources/official-docs.json b/database/resources/official-docs.json index 44fe149b0..4da8f015f 100644 --- a/database/resources/official-docs.json +++ b/database/resources/official-docs.json @@ -75,8 +75,8 @@ "url": "https://svelte.dev/docs/introduction", "category": "resources", "subcategory": "officialdocs" - } , - { + }, + { "name": "Express", "description": "Express is a fast, unopinionated, and minimalist web framework for Node.js", "url": "https://expressjs.com", diff --git a/database/youtube/machine-learning.json b/database/youtube/machine-learning.json index 711481d18..04a6ccd5a 100644 --- a/database/youtube/machine-learning.json +++ b/database/youtube/machine-learning.json @@ -70,5 +70,13 @@ "category": "youtube", "subcategory": "machine-learning", "language": "english" + }, + { + "name": "CodeBasics", + "description": "Learn simple programming, data science, data analytics, artificial intelligence, machine learning, data structures, software architecture and much more.", + "url": "https://youtube.com/@codebasics", + "category": "youtube", + "subcategory": "machine-learning", + "language": "english" } ] diff --git a/database/youtube/web-development.json b/database/youtube/web-development.json index 338861534..13f04004c 100644 --- a/database/youtube/web-development.json +++ b/database/youtube/web-development.json @@ -318,5 +318,21 @@ "category": "youtube", "subcategory": "web-development", "language": "english" + }, + { + "name": "Devtips", + "description": "This channel deep dives into the world of Web Development.", + "url": "https://www.youtube.com/channel/UCyIe-61Y8C4_o-zZCtO4ETQ", + "category": "youtube", + "subcategory": "web-development", + "language": "english" + }, + { + "name": "Joseph Smith", + "description": "Learn programming and building with languages like HTML,CSS ,PHP, and much more.", + "url": "https://www.youtube.com/c/TheHelpingDevelop/", + "category": "youtube", + "subcategory": "web-development", + "language": "english" } ] diff --git a/hooks/useFilterSearch.ts b/hooks/useFilterSearch.ts deleted file mode 100644 index e6dbebf28..000000000 --- a/hooks/useFilterSearch.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { database } from 'database/data' - -export default function useFilterSearch() { - const filterSearch = (query: string) => - database - .map((c) => - c.filter((r) => r.name.toLowerCase().includes(query?.toLowerCase())) - ) - .flat() - - return { filterSearch } -} diff --git a/hooks/useSearch.ts b/hooks/useSearch.ts deleted file mode 100644 index 551e3cda9..000000000 --- a/hooks/useSearch.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useRouter } from 'next/router' -import { useEffect, useRef, useState } from 'react' - -function useSearch() { - const [search, setSearch] = useState('') - const [debouncedSearch, setDebouncedSearch] = useState(search) - const firstRender = useRef(true) - - const router = useRouter() - const query = router.query.query - - useEffect(() => { - const timeout = setTimeout(() => { - setDebouncedSearch(search) - }, 500) - - return () => { - clearTimeout(timeout) - } - }, [search]) - - useEffect(() => { - if (query && firstRender.current) { - setSearch(query as string) - } - }, []) - - return { search, setSearch, debouncedSearch } -} - -export default useSearch diff --git a/hooks/useSearchReducer.ts b/hooks/useSearchReducer.ts new file mode 100644 index 000000000..096a51638 --- /dev/null +++ b/hooks/useSearchReducer.ts @@ -0,0 +1,21 @@ +import { useReducer } from "react" +import { useRouter } from "next/router" + +import { SearchbarState, searchbarReducer } from "components/Searchbar/SearchbarReducer" + +export const useSearchReducer = () => { + const router = useRouter() + const initialQuery = (router.query.query as string) ?? '' + + const initialState: SearchbarState = { + searchQuery: initialQuery, + categoryQuery: initialQuery, + searchQueryIsValid: true, + showSuggestions: false, + } + + return useReducer( + searchbarReducer, + initialState + ) +} \ No newline at end of file diff --git a/hooks/useSidebarSearch.ts b/hooks/useSidebarSearch.ts deleted file mode 100644 index 98c0327f6..000000000 --- a/hooks/useSidebarSearch.ts +++ /dev/null @@ -1,52 +0,0 @@ -import useSearch from './useSearch' -import { sidebarData } from '../database/data' -import { useEffect, useState } from 'react' -import { useRouter } from 'next/router' - -function useSidebarSearch() { - const { setSearch, debouncedSearch } = useSearch() - const [searchResults, setSearchResults] = useState(sidebarData) - const router = useRouter() - - useEffect(() => { - const filteredResults = sidebarData.filter((sidebarItem) => - sidebarItem.subcategory.some((subcategory) => matchSearch(subcategory)) - ) - - const mappedResults = filteredResults.map((sidebarItem) => ({ - ...sidebarItem, - subcategory: sidebarItem.subcategory.filter((subcategory) => - matchSearch(subcategory) - ), - })) - - setSearchResults(mappedResults) - - if (debouncedSearch.length > 0) { - router.push({ - pathname: '/search', - query: { - query: debouncedSearch, - }, - }) - } - }, [debouncedSearch]) - - function matchSearch(item: { name: any; url?: string; resources: any }) { - const itemName = item.name.toLowerCase() - const matchingResources = item.resources.filter( - (resource: { name: string }) => - resource.name.toLowerCase().includes(debouncedSearch.toLowerCase()) - ) - - return ( - debouncedSearch.length < 1 || - itemName.includes(debouncedSearch.toLowerCase()) || - matchingResources.length > 0 - ) - } - - return { setSearch, searchResults, debouncedSearch } -} - -export default useSidebarSearch diff --git a/package.json b/package.json index c7a3b7f46..1e0a8dce1 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "react": "18.2.0", "react-autosuggest": "^10.1.0", "react-dom": "18.2.0", + "react-helmet-async": "^1.3.0", "react-icons": "^4.8.0", "react-spinners": "^0.13.8", "react-tooltip": "^5.20.0", @@ -47,4 +48,4 @@ "postcss": "^8.4.21", "tailwindcss": "^3.3.1" } -} +} \ No newline at end of file diff --git a/pages/[subcategory]/index.tsx b/pages/[subcategory]/index.tsx index 55cc006f6..2c64e8ea2 100644 --- a/pages/[subcategory]/index.tsx +++ b/pages/[subcategory]/index.tsx @@ -27,7 +27,6 @@ const SubCategory = () => { <> {title} - - { const { results, setResults } = useResults() const router = useRouter() const title = `LinksHub - ${router.asPath .charAt(1) .toUpperCase()}${router.asPath.slice(2)}` + const query = router.query.query - const { filterSearch } = useFilterSearch() + const filteredCardsList = useMemo( + () => getFilteredCardsList(query as string), + [query] + ) useEffect(() => { if (!query || query === '') router.replace('/'); }, [query, router]); - let content: JSX.Element[] | JSX.Element - - const data = filterSearch(query as string); - + + const data = filteredCardsList useEffect(() => { if (data.length > 0 && data.length !== -1) { setResults(data.length) @@ -33,18 +37,11 @@ const Search = () => { setResults(0) } }, [data]) - - if (data.length > 0) { - content = - } else { - content = - } - + return ( <> {title} - { results={results} />
    - {content} + {filteredCardsList.length > 0 ? ( + + ) : ( + + )}
    ); }; -export default Search; +const getFilteredCardsList = (query: string) => + database + .map((c) => + c.filter((r) => r.name.toLowerCase().includes(query?.toLowerCase())) + ) + .flat() + +export default Search diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 539b44289..7eddc87e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,8 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false dependencies: '@next/font': @@ -55,6 +59,9 @@ dependencies: react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + react-helmet-async: + specifier: ^1.3.0 + version: 1.3.0(react-dom@18.2.0)(react@18.2.0) react-icons: specifier: ^4.8.0 version: 4.8.0(react@18.2.0) @@ -1548,6 +1555,12 @@ packages: side-channel: 1.0.4 dev: false + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + /is-arguments@1.1.1: resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} engines: {node: '>= 0.4'} @@ -2212,6 +2225,25 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + dev: false + + /react-helmet-async@1.3.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.6.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.21.0 + invariant: 2.2.4 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + dev: false + /react-icons@4.8.0(react@18.2.0): resolution: {integrity: sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==} peerDependencies: @@ -2363,6 +2395,10 @@ packages: resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} dev: false + /shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + dev: false + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2736,7 +2772,3 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/types/index.ts b/types/index.ts index b7faaac66..a413c891d 100644 --- a/types/index.ts +++ b/types/index.ts @@ -44,7 +44,7 @@ export type Category = | 'youtube' | 'other' | 'devops' - | 'competitive_programming' + | 'cp - competitive pro' export type SubCategories = { name: string @@ -92,6 +92,8 @@ export const subcategoryArray = [ 'images', 'online-code-editors', 'react', + 'next-js', + 'three-js', 'themes-templates', 'ui-generators', 'videos',