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 0aa1ec1aa..8f52ba086 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -152,33 +152,7 @@ When adding _YouTube_ channel links, please specify _the language_ of the channe
- 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,9 +172,7 @@ 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
@@ -211,7 +183,7 @@ We follow a standardized commit message format using Commitlint to ensure consis
- `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`
@@ -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/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 (
-
-
-
- handleCopy(e)}
- />
+
+
+
+ {success ? ( // Render the FaCheckSquare icon if success is true
+ handleCopy(e)}
+ aria-label="Link copied" // Add aria-label for accessibility
+ />
+ ) : (
+ handleCopy(e)}
+ aria-label="Copy link to clipboard" // Add aria-label for accessibility
+ />
+ )}
-
+
- )
-}
+ );
+}
\ 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 (
-
)
}
+
+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 (
- {item.category}
+ {category}
- {subcategoryList}
+ {sortedSubcategoryList}
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 (
-
- {resolvedTheme === 'dark' ? (
-
- ) : (
-
+
+
+
+
+
+
- )}
-
- )
+
+
+ {resolvedTheme === 'dark' ? (
+
+ ) : (
+
+ )}
+
+
+ );
}
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/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 faf422e11..05237d75a 100644
--- a/database/frontend/online-code-editors.json
+++ b/database/frontend/online-code-editors.json
@@ -124,5 +124,19 @@
"url": "https://csacademy.com/workspace/",
"category": "frontend",
"subcategory": "online-code-editors"
+ },
+ {
+ "name": "Ideone",
+ "description": " Ideone is an online compiler and debugging tool that allows developers to compile source code and execute it online in more than 60 programming languages.",
+ "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/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/computer-science.json b/database/youtube/computer-science.json
index 1fda9b6db..3c6ec80f2 100644
--- a/database/youtube/computer-science.json
+++ b/database/youtube/computer-science.json
@@ -30,5 +30,13 @@
"category": "youtube",
"subcategory": "computer-science",
"language": "hindi"
+ },
+ {
+ "name": "Neso Academy",
+ "description": " Covers various topics related to field of computer science like Engineering Lectures,School Syllabus, and Competitive Exams",
+ "url": "https://www.youtube.com/@nesoacademy",
+ "category": "youtube",
+ "subcategory": "computer-science",
+ "language": "hindi"
}
]
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 6efd1cc51..13f04004c 100644
--- a/database/youtube/web-development.json
+++ b/database/youtube/web-development.json
@@ -326,5 +326,13 @@
"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',