-
Notifications
You must be signed in to change notification settings - Fork 21
Feature/search and filters #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/search and filters #44
Conversation
@Ankur1493 is attempting to deploy a commit to the Ankur 's projects Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughThe changes introduced in this pull request include the addition of a new configuration file Changes
Tip CodeRabbit's docstrings feature is now available as part of our Early Access Program! Simply use the command Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 28
🧹 Outside diff range and nitpick comments (42)
prisma/migrations/20241123141312_categories_table_temp_added/migration.sql (1)
3-3
: Consider using the UUID data type for the "id" columnCurrently, the
"id"
column is defined asTEXT
. If you are storing UUIDs, consider using theUUID
data type for better data integrity and performance.src/lib/types.ts (3)
2-2
: Re-evaluate the optionality of the "id" fieldIn both
ToolCardInterface
andToolDetailsInterface
, theid
field is optional (id?: string
). If theid
is always present for tools, consider making it required for consistency and to prevent potential undefined errors.Also applies to: 12-12
8-8
: Ensure consistent optionality of the "lastUpdated" fieldIn
ToolCardInterface
,lastUpdated
is optional (lastUpdated?: Date
), but it's required (lastUpdated: Date
) inToolDetailsInterface
. Unless there's a specific reason, consider making the optionality consistent across both interfaces.Also applies to: 18-18
22-23
: Useundefined
instead ofnull
for optional propertiesThe
websiteUrl
anddocumentation
properties are typed asstring | null
. In TypeScript, it's more idiomatic to useundefined
for optional properties:- websiteUrl?: string | null - documentation?: string | null + websiteUrl?: string + documentation?: stringThis change can simplify checks for the existence of these properties.
src/components/tools/AlgoliaSearch.tsx (1)
25-40
: Use thegetTools
function within the componentThe
getTools
function is defined but not utilized within theAlgoliaSearch
component. To fetch and display tools based on the search query, consider using this function inside your component.Apply this diff to integrate
getTools
:const AlgoliaSearch: React.FC<AlgoliaSearchProps> = async ({ searchParams }) => { + + const tools = await getTools(searchParams.query || '') + return ( <div className="w-full max-w-4xl mx-auto"> <ToolsFilter /> + {/* Render tools here */} + <ul> + {tools.map((tool) => ( + <li key={tool.name}>{tool.name}</li> + ))} + </ul> </div> ) }This will fetch tools based on the query and render them in a list.
prisma/schema.prisma (2)
35-55
: Consider using relations forcategories
andtags
Defining
c 8000 ategories
andtags
asString[]
in theTool
model may limit flexibility and data integrity. Using relational models can help normalize data and establish proper relationships.Would you like assistance in refactoring these fields to use relations with
Category
andTag
models?
57-64
: RenameCategories
model to singularCategory
For consistency and clarity, consider renaming the
Categories
model toCategory
. This aligns with conventional naming practices in Prisma models.Apply this diff to rename the model:
-model Categories { +model Category { id String @id @default(uuid()) name String @unique count Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }src/components/ui/popover.tsx (1)
23-26
: Extract long class names into a constant for readabilityThe
className
string inPopoverContent
is quite long, which may affect readability and maintainability. Consider extracting it into a constant or utilizing a styling solution like CSS modules or styled-components.Apply this diff to extract the class names:
+const popoverContentClasses = cn( + "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none", + "data-[state=open]:animate-in data-[state=closed]:animate-out", + "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", + "data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2", + "data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" +) const PopoverContent = React.forwardRef< React.ElementRef<typeof PopoverPrimitive.Content>, React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( <PopoverPrimitive.Portal> <PopoverPrimitive.Content ref={ref} align={align} sideOffset={sideOffset} - className={cn( - "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className - )} + className={cn(popoverContentClasses, className)} {...props} /> </PopoverPrimitive.Portal> ))src/components/tools/ToolSkeleton.tsx (1)
6-6
: Simplify className by removing unnecessary template literalsThe
className
properties on lines 6 and 8 use template literals with interpolation syntax, but no variables are interpolated. You can simplify the code by using plain string literals.Apply this diff to simplify your code:
- className={`relative w-full h-full max-w-sm bg-gradient-to-br from-[#050817] to-gray-950 border-white border-opacity-10 rounded-xl overflow-hidden`} + className="relative w-full h-full max-w-sm bg-gradient-to-br from-[#050817] to-gray-950 border-white border-opacity-10 rounded-xl overflow-hidden"and
- <div className={`absolute inset-0 skeleton-shimmer`}></div> + <div className="absolute inset-0 skeleton-shimmer"></div>Also applies to: 8-8
src/app/api/tools/similar/route.ts (2)
11-11
: SimplifynotIn
tonot
for a single valueSince
slug
is a single string, you can simplify the condition by usingnot: slug
instead ofnotIn: [slug]
.Apply this diff:
- { name: { notIn: [slug] } }, + { name: { not: slug } },Also applies to: 25-25
11-11
: Remove outdated commentsThe comments
// Changed from 'not' to 'notIn'
on lines 11 and 25 appear to be outdated or irrelevant in the current code context. Consider removing them to keep the code clean.Also applies to: 25-25
src/lib/tools.ts (2)
35-35
: Log the error in the catch block for better debuggingIn the catch block, you're returning an error response but not logging the error. Add
console.error(err);
to help with debugging.Add the following line:
} catch (err) { + console.error('Error in getToolDetails:', err); return {
62-62
: Simplify error logging by logging the entire error objectThe error handling code assumes that
error.response
may exist, which may not be the case. Log the entire error object to ensure all relevant information is captured.Modify the logging line:
- console.error('Error in API:', error.response?.data || error.message); + console.error('Error in getToolCategories:', error);src/app/tools/page.tsx (1)
32-33
: Correct the grammatical error and remove unnecessary string interpolationThe phrase "well researched" should be hyphenated to "well-researched". Also, the use of
{" "}
to insert a space is unnecessary; you can include the space directly in the string.Apply this diff to correct the issues:
- Discover new devtools from a well researched collection for hassle{" "} - free development of your next product + Discover new devtools from a well-researched collection for hassle-free development of your next productsrc/app/api/tools/route.ts (2)
18-18
: Avoid using 'any' type; define a proper type for 'where' conditionUsing
any
bypasses TypeScript's type checking. Defining a specific type forwhere
will improve code safety and maintainability.Consider defining the
where
variable with an appropriate type from Prisma, such asPrisma.ToolWhereInput
:- const where: any = {}; + const where: Prisma.ToolWhereInput = {};Don't forget to import
Prisma
from the Prisma client:import { Prisma } from '@prisma/client';
47-47
: Remove unnecessary 'recent' case in switch statementThe
case 'recent':
is redundant because thedefault
case covers the same logic. Removing it simplifies the code without changing functionality.Apply this diff to remove the redundant case:
break; - case 'recent': default: orderBy = { lastUpdated: 'desc' };
🧰 Tools
🪛 Biome (1.9.4)
[error] 47-47: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
src/components/tools/toolDetails/SimilarTools.tsx (2)
40-43
: Implement user-friendly error handlingCurrently, errors during the API call are only logged to the console. Consider displaying an error message to inform the user that similar tools could not be loaded.
Add an error state and update the catch block:
+ const [error, setError] = useState(false); try { // existing code } catch (error) { console.error("Error fetching similar tools:", error); + setError(true); } finally { setIsLoading(false); }Display the error message in the render:
if (isLoading) { return (/* existing loading skeleton */); + } else if (error) { + return <div>Error loading similar tools. Please try again later.</div>; } else { // existing render code }
65-87
: Show message when no similar tools are foundIf both
similarTagTools
andsimilarCategoriesTools
are empty, the component renders nothing, which might confuse users. Consider displaying a message indicating that no similar tools are available.Add a fallback message:
</div> + {similarTagTools.length === 0 && similarCategoriesTools.length === 0 && ( + <p>No similar tools found.</p> + )} </div>src/components/tools/Categories.tsx (1)
111-111
: Correct the label to reflect the item countThe label reads
{category.count || 0} Categories
, which may be misleading ifcategory.count
represents the number of tools in a category. Update the label to accurately reflect the count.Change the text to:
<p className="text-sm text-muted-foreground"> - {category.count || 0} Categories + {category.count || 0} Tools </p>src/components/tools/toolDetails/ToolDetailsPage.tsx (7)
22-35
: Consider making the features list dynamic.The
features
array is hardcoded within the component. To enhance reusability and scalability, consider fetching features dynamically from thetool
data or an API, especially if different tools have different features.
37-42
: Make language colors dynamic based on actual languages used.The
languageColors
object is hardcoded and may not cover all languages used by different tools. Consider fetching language colors dynamically or using a library that maps programming languages to colors to ensure accuracy.
44-49
: Retrieve language statistics dynamically for accuracy.The
languages
object is currently hardcoded, which may not accurately reflect the language breakdown for each tool. Consider obtaining this data from thetool
prop or an API to display accurate language statistics for each tool.
221-225
: Avoid hardcoding image URLs; use dynamic images instead.The image source is hardcoded to a specific URL. To enhance flexibility and maintainability, consider using an image URL from the
tool
data or fetching it dynamically.Here's a suggested fix:
- <img - src="https://tailwind-elements.com/docs/standard/designblocks/hero-sections/assets/hero-3-dark.webp" - alt={`${tool.name} interface`} - className="rounded mb-6" - /> + {tool.image && ( + <img + src={tool.image} + alt={`${tool.name} interface`} + className="rounded mb-6" + /> + )}
2-12
: Optimize icon imports to reduce bundle size.Currently, multiple icons are imported from
lucide-react
, which could increase the bundle size if unused icons are included. Consider importing only the icons used in this component or using tree-shaking to optimize performance.
75-75
: Avoid redundant display oftool.description
.The
tool.description
is displayed both in the summary section and the "About" section. To improve user experience, consider removing one instance or providing additional unique content.Also applies to: 205-205
208-219
: Externalize static content for better maintainability.The "About" section contains hardcoded text with multiple references to
{tool.name}
. If this content is generic or used across multiple tools, consider externalizing it to a separate file or fetching it from an API to improve maintainability.src/app/api/tools/categories/route.ts (1)
11-11
: Correct the Error Message for No Categories FoundThe message 'No tools found' should be updated to 'No categories found' to accurately reflect the resource being accessed.
Apply this change:
return NextResponse.json( - { message: 'No tools found' }, + { message: 'No categories found' }, { status: 404 } );src/app/tools/[slug]/page.tsx (1)
8-11
: Enhance SEO with Dynamic MetadataCurrently, the
metadata
is static. Consider setting themetadata
dynamically based on thetoolDetails
to improve SEO and provide more descriptive titles and descriptions for each tool's page.You can achieve this by using the
generateMetadata
function provided by Next.js. Here's how you might implement it:-import { Metadata } from 'next'; +import { Metadata, ResolvingMetadata } from 'next'; ... -export const metadata: Metadata = { - title: 'Dev Tools Academy', - description: 'DevToolsAcademy Tool details', -}; -export default async function ToolDetailRoute({ params }: { params: { slug: string } }) { +export async function generateMetadata( + { params }: { params: { slug: string } }, + parent: ResolvingMetadata +) { + const response = await getToolDetails(params.slug); + const toolDetails = response.toolDetails; + + if (!toolDetails) { + return { + title: 'Tool Not Found - Dev Tools Academy', + description: 'The requested tool could not be found.', + }; + } + + return { + title: `${toolDetails.name} - Dev Tools Academy`, + description: toolDetails.description, + }; +} + +export default async function ToolDetailRoute({ params }: { params: { slug: string } }) { const response = await getToolDetails(params.slug) const toolDetails = response.toolDetails if (!toolDetails) { return ( <ToolNotFound /> ) } return ( <main className="min-h-screen w-full"> <ToolDetailsPage tool={toolDetails} /> </main> ); }This way, the page's metadata is dynamically generated based on the tool's details.
prisma/migrations/20241119120057_tool_temp_added/migration.sql (2)
16-16
: Ensure 'updatedAt' Column Automatically UpdatesThe "updatedAt" column does not have a default value or an automatic update mechanism. To accurately track when a record is modified, consider implementing a trigger or updating this field in your application logic whenever the row is updated.
In your Prisma schema, you can define the
updatedAt
field with the@updatedAt
attribute:updatedAt TIMESTAMP(3) NOT NULL, + /* In Prisma schema, this would be: */ + updatedAt DateTime @updatedAtThis will automatically update the
updatedAt
field whenever the record is modified.
25-25
: Consider Database Design for Categories and TagsIndexing array columns like
categories
andtags
may not provide efficient query performance. For better scalability and query optimization, consider normalizing your database schema using many-to-many relationships with join tables forcategories
andtags
.Note: As per the retrieved learnings, the
Tool
model and its indexes are temporary placeholders. Keep this advice in mind when designing the final database schema.Also applies to: 28-28, 31-31
src/app/tools/[slug]/loading.tsx (3)
3-4
: Impr 8000 ove mobile responsiveness with proper paddingThe container lacks proper padding for mobile screens. Consider adding base padding for mobile views.
- <div className="w-full h-screen flex flex-col justify-start items-start md:px-72 pt-16"> + <div className="w-full h-screen flex flex-col justify-start items-start px-4 md:px-72 pt-16">
13-30
: Consider extracting repeated layout patterns into componentsThe flex container structure is duplicated. Consider creating a reusable component for these sections and using CSS Grid for better layout control.
Example component structure:
type SkeletonGroupProps = { title: string; count: number; }; function SkeletonGroup({ title, count }: SkeletonGroupProps) { return ( <div className="flex flex-col flex-1 gap-2"> <div className={`${skeletonClass} w-20 h-6`}></div> <div className="flex items-center gap-4 rounded-lg w-full"> {Array.from({ length: count }).map((_, i) => ( <div key={i} className={`${skeletonClass} w-20 h-6`}></div> ))} </div> </div> ); }
1-2
: Enhance code organization and type safetyConsider adding TypeScript types and proper documentation to improve maintainability.
+import { FC } from 'react'; + +/** + * Loading skeleton component for the tool details page. + * Displays a placeholder UI while the actual content is being loaded. + */ -export default function Loading() { +const Loading: FC = () => { return ( // ... component content ); -} +}; + +export default Loading;Also applies to: 58-59
src/app/layout.tsx (2)
33-40
: Consider adding more favicon sizes for better device support.The current favicon configuration only includes a 32x32 size. Consider adding more sizes (16x16, 48x48, 192x192, 512x512) to support various devices and scenarios (PWA, mobile bookmarks, etc.).
icons: [ { rel: 'icon', type: 'image/png', sizes: '32x32', url: '/favicon.png', }, + { + rel: 'icon', + type: 'image/png', + sizes: '16x16', + url: '/favicon-16x16.png', + }, + { + rel: 'icon', + type: 'image/png', + sizes: '192x192', + url: '/favicon-192x192.png', + }, + { + rel: 'apple-touch-icon', + sizes: '180x180', + url: '/apple-touch-icon.png', + }, ],
48-53
: Consider adding JSDoc documentation.The function handles different title formats well, but could benefit from documentation explaining the different cases it handles.
+/** + * Extracts the title from the metadata object. + * @param title - The title from metadata, can be either a string or an object with a default property + * @returns The extracted title string or a default value + */ function getTitle(title: Metadata['title']): string {src/components/tools/ToolsPagination.tsx (5)
26-26
: Remove console.log statement.Debug logging should be removed before production deployment.
-console.log({ totalPages })
21-25
: Add validation for basePath parameter.The URL creation doesn't validate the basePath format, which could lead to invalid URLs if the input is malformed.
const createPageUrl = (pageNumber: number) => { + const cleanBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`; const params = new URLSearchParams(searchParams.toString()); params.set('page', pageNumber.toString()); - return `${basePath}?${params.toString()}`; + return `${cleanBasePath}?${params.toString()}`; };
28-49
: Improve maintainability and performance of page number calculation.Consider the following improvements:
- Define constants for ellipsis markers
- Memoize the calculation to prevent unnecessary recalculations
+const ELLIPSIS_START = -1; +const ELLIPSIS_END = -2; + +const getPageNumbers = React.useMemo(() => { -const getPageNumbers = () => { const pages = []; if (totalPages > 0) pages.push(1); const startPage = Math.max(2, currentPage - 2); const endPage = Math.min(totalPages, currentPage + 2); - if (startPage > 2) pages.push(-1); + if (startPage > 2) pages.push(ELLIPSIS_START); for (let i = startPage; i <= endPage; i++) { if (i !== 1 && i !== totalPages) { pages.push(i); } } - if (endPage < totalPages - 1) pages.push(-2); + if (endPage < totalPages - 1) pages.push(ELLIPSIS_END); if (totalPages > 1) pages.push(totalPages); return pages; -}; +}, [currentPage, totalPages]);
51-94
: Enhance accessibility with descriptive labels.While the current implementation includes basic accessibility attributes, it could be improved with more descriptive aria-labels.
<PaginationPrevious href={currentPage > 1 ? createPageUrl(currentPage - 1) : undefined} aria-disabled={currentPage <= 1} + aria-label={`Go to page ${currentPage - 1}`} className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""} /> <PaginationNext href={currentPage < totalPages ? createPageUrl(currentPage + 1) : undefined} aria-disabled={currentPage >= totalPages} + aria-label={`Go to page ${currentPage + 1}`} className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} />
17-96
: Consider adding error boundary for pagination failures.As this component handles URL parameters and calculations that could potentially fail, consider wrapping it in an error boundary component to gracefully handle and recover from runtime errors.
src/components/tools/SearchBar.tsx (2)
12-14
: Consider adding input validationThe search input could benefit from basic sanitization to prevent injection or handle special characters appropriately.
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { - setSearchValue(e.target.value) + const sanitizedValue = e.target.value.replace(/[<>]/g, '') + setSearchValue(sanitizedValue) }
34-48
: LGTM! Well-implemented effects with proper cleanupThe debouncing implementation is solid. Consider extracting the debounce time as a constant for easier configuration:
+const SEARCH_DEBOUNCE_MS = 300 + useEffect(() => { const debounceSearch = setTimeout(() => { updateSearchQuery(searchValue.trim()) - }, 300) // Increased debounce time for better performance + }, SEARCH_DEBOUNCE_MS) return () => clearTimeout(debounceSearch) }, [searchValue, updateSearchQuery])
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (32)
components.json
(1 hunks)package.json
(2 hunks)prisma/migrations/20241119120057_tool_temp_added/migration.sql
(1 hunks)prisma/migrations/20241123141312_categories_table_temp_added/migration.sql
(1 hunks)prisma/schema.prisma
(1 hunks)src/app/api/tools/categories/route.ts
(1 hunks)src/app/api/tools/route.ts
(1 hunks)src/app/api/tools/similar/route.ts
(1 hunks)src/app/layout.tsx
(1 hunks)src/app/page.tsx
(1 hunks)src/app/tools/[slug]/loading.tsx
(1 hunks)src/app/tools/[slug]/page.tsx
(1 hunks)src/app/tools/layout.tsx
(1 hunks)src/app/tools/loading.tsx
(1 hunks)src/app/tools/page.tsx
(1 hunks)src/components/tools/AlgoliaSearch.tsx
(1 hunks)src/components/tools/Categories.tsx
(1 hunks)src/components/tools/LoadingCategories.tsx
(1 hunks)src/components/tools/SearchBar.tsx
(1 hunks)src/components/tools/ToolCard.tsx
(1 hunks)src/components/tools/ToolSkeleton.tsx
(1 hunks)src/components/tools/ToolsFilter.tsx
(1 hunks)src/components/tools/ToolsPage.tsx
(1 hunks)src/components/tools/ToolsPagination.tsx
(1 hunks)src/components/tools/toolDetails/SimilarTools.tsx
(1 hunks)src/components/tools/toolDetails/ToolDetailsPage.tsx
(1 hunks)src/components/tools/toolDetails/ToolNotFound.tsx
(1 hunks)src/components/ui/pagination.tsx
(1 hunks)src/components/ui/popover.tsx
(1 hunks)src/lib/tools.ts
(1 hunks)src/lib/types.ts
(1 hunks)tailwind.config.ts
(1 hunks)
✅ Files skipped from review due to trivial changes (6)
- tailwind.config.ts
- src/app/page.tsx
- src/app/tools/loading.tsx
- src/components/tools/LoadingCategories.tsx
- src/components/tools/toolDetails/ToolNotFound.tsx
- components.json
🧰 Additional context used
📓 Learnings (3)
prisma/schema.prisma (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#39
File: prisma/schema.prisma:52-54
Timestamp: 2024-11-23T12:12:03.854Z
Learning: In `prisma/schema.prisma`, the `Tool` model and its indexes are temporary placeholders, and the proper database structure will be added later.
src/app/tools/[slug]/page.tsx (2)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#39
File: src/app/tools/[slug]/page.tsx:13-35
Timestamp: 2024-11-23T12:00:26.545Z
Learning: In `src/app/tools/[slug]/page.tsx`, the page is server-side rendered and `toolDetails` are passed as props, so adding client-side loading states using React Suspense isn't necessary.
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#39
File: src/app/tools/[slug]/page.tsx:15-16
Timestamp: 2024-11-23T12:04:31.686Z
Learning: In `src/app/tools/[slug]/page.tsx`, since `getToolDetails` is a server action, explicit error handling with try-catch is not necessary.
prisma/migrations/20241119120057_tool_temp_added/migration.sql (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#39
File: prisma/schema.prisma:52-54
Timestamp: 2024-11-23T12:12:03.854Z
Learning: In `prisma/schema.prisma`, the `Tool` model and its indexes are temporary placeholders, and the proper database structure will be added later.
🪛 Biome (1.9.4)
src/app/api/tools/route.ts
[error] 47-47: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
🔇 Additional comments (10)
src/app/tools/layout.tsx (1)
1-8
: Code looks good!
The ToolsLayout
component is correctly defined and structured. The use of children
prop and the background styling is appropriate.
src/components/tools/AlgoliaSearch.tsx (1)
3-3
: Verify component compatibility between server and client components
Importing ToolsFilter
into AlgoliaSearch
may cause issues if ToolsFilter
is a client component and AlgoliaSearch
is a server component. Ensure that AlgoliaSearch
is declared as a client component if necessary.
Consider adding the "use client"
directive at the top of the file if AlgoliaSearch
needs to be a client component:
+"use client"
import React from 'react'
Alternatively, if ToolsFilter
is a server component, no action is needed.
package.json (1)
24-24
: New dependencies added successfully
The new dependencies are properly added to the dependencies
section.
Also applies to: 30-30, 47-47
src/components/ui/pagination.tsx (1)
1-117
: Well-structured and accessible pagination components
The pagination components are well-implemented with proper accessibility attributes (aria-label
, aria-current
) and semantic HTML elements. The use of React.forwardRef
and TypeScript typing enhances maintainability and usability.
src/app/layout.tsx (3)
44-44
: LGTM!
The interface is properly typed and follows TypeScript best practices.
75-85
: LGTM! Well-structured layout implementation.
The layout implementation follows Next.js best practices with:
- Proper font configuration
- Semantic HTML structure
- Accessible container structure
- Proper analytics integration
69-73
: Verify Umami analytics configuration.
The analytics script uses an environment variable. Ensure it's properly configured in all environments.
src/components/tools/ToolsPagination.tsx (1)
1-15
: LGTM! Well-structured imports and interface definition.
The component's dependencies and type definitions are clear and properly organized.
src/components/tools/SearchBar.tsx (2)
1-10
: LGTM! Component setup is well-structured
The component is properly configured for client-side rendering with appropriate hooks and state initialization from URL parameters.
16-32
: Verify UX behavior and enhance URL handling
The current implementation clears all filters when performing a search. While this might simplify the user experience, it could be unexpected for users who want to combine search with filters.
Additionally, consider enhancing URL parameter handling:
const updateSearchQuery = useCallback((query: string) => {
const params = new URLSearchParams(searchParams)
const hasFilters = params.has("categories") || params.has("tags") || params.has("sort")
if (query) {
- if (hasFilters) {
- params.delete("categories")
- params.delete("tags")
- params.delete("sort")
- }
params.set('query', query)
} else {
params.delete('query')
}
+ // Ensure clean URLs by removing empty parameters
+ Array.from(params.keys()).forEach(key => {
+ const value = params.get(key)
+ if (!value || value.length === 0) params.delete(key)
+ })
router.push(`/tools?${params.toString()}`)
}, [router, searchParams])
Let's verify the current behavior's impact on user experience:
✅ Verification successful
Keep filter clearing behavior and enhance URL handling
The search results reveal that the current behavior of clearing filters during search is intentionally designed as part of a reciprocal relationship between search and filters:
- In
ToolsFilter.tsx
, when filters are applied, the search query is cleared:params.delete("query")
- Similarly, in
SearchBar.tsx
, when searching, filters are cleared.
This mutual exclusivity appears to be a deliberate UX pattern in the application. However, the URL parameter cleanup suggestion is still valuable:
const updateSearchQuery = useCallback((query: string) => {
const params = new URLSearchParams(searchParams)
const hasFilters = params.has("categories") || params.has("tags") || params.has("sort")
if (query) {
if (hasFilters) {
params.delete("categories")
params.delete("tags")
params.delete("sort")
}
params.set('query', query)
} else {
params.delete('query')
}
+ // Ensure clean URLs by removing empty parameters
+ Array.from(params.keys()).forEach(key => {
+ const value = params.get(key)
+ if (!value || value.length === 0) params.delete(key)
+ })
router.push(`/tools?${params.toString()}`)
}, [router, searchParams])
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if there are any existing implementations combining search with filters
rg -l "searchParams.*categories.*query|query.*categories" --type ts
Length of output: 69
Script:
#!/bin/bash
# Let's try a broader search to understand the search and filter interaction patterns
# First, let's check for files that handle both search queries and categories/filters
rg "query.*categories|categories.*query" --type ts -A 5
# Also check for any components or hooks that might handle filtering logic
fd -e ts -e tsx -x rg -l "useFilter|handleFilter|applyFilter"
# Look for URL parameter handling patterns
rg "URLSearchParams.*set|params\.set" --type ts -A 3
Length of output: 2022
prisma/migrations/20241123141312_categories_table_temp_added/migration.sql
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🧹 Outside diff range and nitpick comments (7)
src/lib/ZodSchema.ts (1)
3-7
: Consider enhancing slug validationWhile the current validation ensures non-empty slugs, consider adding format validation to ensure consistent URL-friendly slugs.
- slug: z.string().min(1, { message: "slug cannot be less than 1 character" }), + slug: z.string() + .min(1, { message: "slug cannot be less than 1 character" }) + .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, { + message: "slug must be lowercase alphanumeric with optional hyphens" + }),src/components/tools/ToolsFilter.tsx (1)
43-62
: Add loading state when updating filtersThe filter updates should show a loading state to prevent multiple rapid clicks.
+ const [isUpdating, setIsUpdating] = useState(false); const updateFilters = async (type: "categories" | "tags", value: string) => { + if (isUpdating) return; + setIsUpdating(true); const updateState = type === "categories" ? setSelectedCategories : setSelectedTags; // ... rest of the function updateURLWithParams(params); + setIsUpdating(false); };src/components/tools/toolDetails/ToolDetailsPage.tsx (1)
247-253
: Improve loading state for Similar Tools component.The current loading fallback is too simple and might cause layout shifts.
Consider implementing a skeleton loader that matches the final content layout:
const SimilarToolsSkeleton = () => ( <div className="grid grid-cols-1 md:grid-cols-3 gap-4 animate-pulse"> {[1, 2, 3].map((i) => ( <div key={i} className="h-48 bg-gray-800 rounded-lg" /> ))} </div> );- <Suspense fallback={<div>loading...</div>}> + <Suspense fallback={<SimilarToolsSkeleton />}>src/app/api/tools/route.ts (4)
18-37
: Add type safety to the where clause constructionUsing
any
type for the where clause loses type safety. Consider using Prisma's generated types.Apply this diff:
- const where: any = {}; + const where: Prisma.ToolWhereInput = {};Add this import at the top:
+import { Prisma } from '@prisma/client';
39-50
: Simplify switch statement by removing redundant caseThe 'recent' case is redundant as it's handled by the default case.
Apply this diff:
switch (sort) { case 'popular': orderBy = { stars: 'desc' }; break; case 'alphabetical': orderBy = { name: 'asc' }; break; - case 'recent': default: orderBy = { lastUpdated: 'desc' }; }
🧰 Tools
🪛 Biome (1.9.4)
[error] 47-47: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
52-71
: Consider adding database indexes for better query performanceThe query sorts by
stars
,name
, andlastUpdated
. Adding indexes for these fields would improve query performance.Consider adding these indexes to your Prisma schema:
model Tool { // ... existing fields ... @@index([stars]) @@index([name]) @@index([lastUpdated]) }
86-92
: Enhance error handling with specific error typesThe current error handling is generic. Consider categorizing errors and providing more specific error messages.
Apply this diff:
- } catch (error: any) { - console.error('Error in API:', error.response?.data || error.message); - return NextResponse.json( - { error: 'Error processing request. Please try again.' }, - { status: error.response?.status || 500 } - ); + } catch (error) { + console.error('Error in API:', error); + if (error instanceof Prisma.PrismaClientKnownRequestError) { + return NextResponse.json( + { error: 'Database operation failed. Please try again.' }, + { status: 500 } + ); + } + if (error instanceof Prisma.PrismaClientValidationError) { + return NextResponse.json( + { error: 'Invalid query parameters.' }, + { status: 400 } + ); + } + return NextResponse.json( + { error: 'Internal server error.' }, + { status: 500 } + ); }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
⛔ Files ignored due to path filters (1)
package-lock.json
is excluded by!**/package-lock.json
📒 Files selected for processing (8)
package.json
(2 hunks)src/app/api/tools/route.ts
(1 hunks)src/app/api/tools/similar/route.ts
(1 hunks)src/components/tools/ToolCard.tsx
(1 hunks)src/components/tools/ToolsFilter.tsx
(1 hunks)src/components/tools/toolDetails/SimilarTools.tsx
(1 hunks)src/components/tools/toolDetails/ToolDetailsPage.tsx
(1 hunks)src/lib/ZodSchema.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
- src/app/api/tools/similar/route.ts
- package.json
- src/components/tools/toolDetails/SimilarTools.tsx
- src/components/tools/ToolCard.tsx
🧰 Additional context used
🪛 Biome (1.9.4)
src/app/api/tools/route.ts
[error] 47-47: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 7-7: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.
See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.
(lint/suspicious/noGlobalIsNan)
🔇 Additional comments (4)
src/components/tools/ToolsFilter.tsx (2)
70-81
: Ensure clearFilters
resets all filters and uses consistent routing
The clearFilters
function uses window.location.search
instead of the searchParams
hook.
37-41
: Verify search parameter handling
Let's verify that all search parameters are properly handled and maintained.
✅ Verification successful
Search parameter handling is properly implemented and consistent
The verification shows that search parameters are handled consistently across the codebase:
- All relevant parameters (page, query, categories, tags, sort) are properly managed in both frontend components and API routes
- Parameter updates follow a consistent pattern:
ToolsFilter.tsx
resets page to "1" when filters change- Query parameter is explicitly handled (set/delete) in
SearchBar.tsx
- URL construction is consistent using
params.toString()
- Parameter synchronization is maintained between components through URL state
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Description: Check for consistent search parameter handling across the codebase
# Check for all URL parameter usage
rg -A 3 "searchParams.get\("
# Check for parameter update patterns
rg -A 3 "params.set\("
# Check for proper URL construction
rg -A 3 "router.push\("
Length of output: 5662
src/components/tools/toolDetails/ToolDetailsPage.tsx (1)
141-143
: Ensure numerical values for tool.stars
and tool.forks
.
src/app/api/tools/route.ts (1)
1-4
: LGTM! Clean imports and well-typed function signature.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@Ankur1493 when I tried to test the changes locally I found a couple of issues:
check on priority let merge this today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Outside diff range and nitpick comments (1)
src/components/Navbar.tsx (1)
36-43
: Add aria-label to improve accessibility of the GitHub buttonWhile the implementation is good, consider adding an aria-label to improve accessibility for screen readers.
<Button onClick={() => window.open("https://github.com/tyaga001/devtoolsacademy", "_blank", "noopener,noreferrer")} variant="outline" + aria-label="Star DevTools Academy on GitHub" className="flex items-center space-x-2 bg-transparent border border-purple-400 text-purple-400 hover:bg-purple-400 hover:text-gray-900 transition duration-150 ease-in-out" >
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
📒 Files selected for processing (1)
src/components/Navbar.tsx
(1 hunks)
🔇 Additional comments (3)
src/components/Navbar.tsx (3)
10-18
: LGTM! The Tools navigation item has been properly integrated.
The addition of the Tools navigation item aligns with the PR objectives and follows the existing pattern.
56-68
: LGTM! Mobile menu button implementation is accessible and well-structured.
The mobile menu button follows accessibility best practices with proper aria attributes and focus management.
17-17
: Verify Tools page routing and implementation
The Tools navigation item has been added as requested in the PR objectives. Let's verify the implementation:
✅ Verification successful
Tools page implementation and routing are properly set up
The Tools navigation item is correctly implemented with proper routing and components:
/src/app/tools/page.tsx
exists and imports the mainToolsPage
component- The app follows Next.js 13+ file-based routing structure with proper loading states (
loading.tsx
) - Complete implementation includes components for pagination, filtering, and tool details
- Dynamic routing is supported via
[slug]
for individual tool pages
🏁 Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash
# Check if the Tools page component exists and is properly implemented
fd -t f "tools/page.tsx" "src/app"
# Check for any potential routing issues
rg -l "useRouter.*tools" "src/app"
Length of output: 67
Script:
#!/bin/bash
# Let's check if the Tools page exists with different possible naming patterns
fd -t f -e tsx -e ts -e jsx -e js "tools" "src/app"
# Check for any route definitions or configurations
fd -t f "route" "src/app"
# Look for any Tools-related component implementations
rg -i "tools.*page|page.*tools" --type ts --type tsx
# Check for any router configurations or route definitions
rg -i "route.*tools|tools.*route" --type ts --type tsx
Length of output: 436
Script:
#!/bin/bash
# Let's check for the Tools page component with a broader search
fd "tools" "src"
# Look for Tools page implementation without file type restrictions
rg -i "tools.*page|page.*tools"
# Check the app directory structure
fd -t d "tools" "src/app"
# Check if there are any route handlers or components in the tools directory
rg -l "export.*default" "src/app/tools"
Length of output: 1653
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 7
🧹 Outside diff range and nitpick comments (14)
src/components/tools/ToolsFilter.tsx (5)
39-45
: Add error handling for malformed URL parametersThe current implementation assumes URL parameters are well-formed. Consider adding validation and error handling:
const [selectedCategories, setSelectedCategories] = useState<string[]>( - searchParams.get("categories")?.split(",").filter(Boolean) || [] + validateAndFilterParams(searchParams.get("categories"), TOOL_CATEGORIES) ); const [selectedTags, setSelectedTags] = useState<string[]>( - searchParams.get("tags")?.split(",").filter(Boolean) || [] + validateAndFilterParams(searchParams.get("tags"), TOOL_TAGS) );Add this utility function:
function validateAndFilterParams(param: string | null, allowedValues: string[]): string[] { if (!param) return []; return param.split(",") .filter(Boolean) .filter(value => allowedValues.includes(value)); }
93-96
: Use searchParams instead of window.location.searchFor consistency with the rest of the code, use the searchParams object:
- const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams(searchParams.toString());
55-59
: Consider debouncing URL updatesMultiple rapid filter changes could lead to unnecessary route updates. Consider implementing debouncing:
import { debounce } from 'lodash'; const debouncedUpdateURL = debounce((params: URLSearchParams) => { params.set("page", "1"); params.delete("query"); router.push(`/tools?${params.toString()}`); }, 300);
232-238
: Improve accessibility of remove filter buttonsThe 'x' character isn't very accessible. Consider using a more semantic close icon:
<button onClick={() => updateFilters("tags", item)} className="hover:text-red-400 transition-colors duration-200" aria-label={`Remove ${item} filter`} > - x + <svg + className="w-4 h-4" + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + <path + strokeLinecap="round" + strokeLinejoin="round" + strokeWidth={2} + d="M6 18L18 6M6 6l12 12" + /> + </svg> </button>
199-213
: Add keyboard navigation for sort optionsThe sort options menu should be navigable via keyboard:
{["recent", "popular", "alphabetical"].map((option, index) => ( <button key={option} + role="menuitem" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + updateSort(option); + setSelectedSort(option); + } + }} className="px-3 py-2 text-left hover:bg-gray-100 hover:bg-opacity-10 rounded transition-colors duration-200 capitalize flex items-center justify-between text-xs md:text-sm" onClick={() => { updateSort(option); setSelectedSort(option); }} >src/components/tools/Categories.tsx (2)
19-56
: Move helper functions to a separate utility fileConsider extracting these utility functions to a separate file (e.g.,
src/lib/utils.ts
) to improve code organization and reusability.+ // src/lib/utils.ts + export const getRandomColor = () => { ... } + export const getRandomIcon = () => { ... } + export const capitalizeFirstLetter = (string: string) => { ... } - // Current file - const getRandomColor = () => { ... } - const getRandomIcon = () => { ... } - const capitalizeFirstLetter = (string: string) => { ... } + import { getRandomColor, getRandomIcon, capitalizeFirstLetter } from "@/lib/utils";
58-121
: Add TypeScript interfaces and documentationConsider adding TypeScript interfaces for better type safety and JSDoc documentation for better code documentation.
+ interface Category { + name: string; + count: number; + } + interface ToolCategoriesResponse { + status: boolean; + categories: Category[]; + } + /** + * Categories component displays a grid of tool categories with their counts + * @returns JSX.Element + */ - const Categories = async () => { + const Categories = async (): Promise<JSX.Element> => { - const response = await getToolCategories(); + const response: ToolCategoriesResponse = await getToolCategories();prisma/schema.prisma (2)
35-57
: Tool model structure looks good for MVPThe model includes essential fields and appropriate indexes for search functionality. Good use of
cuid()
for ID generation and proper timestamp handling.Consider these future improvements:
- Add min/max length constraints for
name
,headline
, anddescription
- Consider adding a relation to
Categories
model instead of usingString[]
for categories- Add URL validation for
githubUrl
,websiteUrl
, anddocumentation
59-66
: Consider adding validation for Categories modelThe model structure is clean, but could benefit from data validation.
Consider adding these constraints:
model Categories { id String @id @default(uuid()) - name String @unique + name String @unique @db.VarChar(50) - count Int + count Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }src/components/tools/ToolCard.tsx (2)
17-21
: Consider adding error handling for tool logosWhile the logo URLs are controlled by the backend (as per learnings), we should still handle failed image loads gracefully.
<img src={`/images/logo/${tool.name}.png`} alt={`${tool.name} logo`} + onError={(e) => { + e.currentTarget.src = '/images/default-tool-logo.png'; + }} className="w-10 h-10 object-cover border border-white border-opacity-10 rounded" />
13-14
: Add aria-label for better accessibilityThe link should have a descriptive aria-label for screen readers.
- <Link href={`/tools/${encodeURIComponent(tool.name)}`} className="block"> + <Link + href={`/tools/${encodeURIComponent(tool.name)}`} + className="block" + aria-label={`View details for ${tool.name}`} + >src/components/tools/toolDetails/ToolDetailsPage.tsx (2)
142-148
: Improve date handling for lastUpdated.The current date formatting logic could be moved to a reusable utility function and include better error handling.
Apply this diff:
+const formatDate = (date: string | Date) => { + try { + return new Date(date).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + } catch { + return "N/A"; + } +}; -{tool.lastUpdated - ? new Date(tool.lastUpdated).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }) - : "N/A"} +{formatDate(tool.lastUpdated)}
153-176
: Remove or document commented-out code.Large blocks of commented-out code make the codebase harder to maintain. Either remove this code if it's no longer needed or document why it's being preserved.
src/app/api/tools/route.ts (1)
1014-1020
: Improve error handling with specific error messages.The current error handling doesn't provide specific information about what went wrong, making debugging difficult.
Apply this diff:
} catch (error: any) { - console.error('Error in API:', error.message); + const errorMessage = error instanceof Error + ? error.message + : 'Unknown error occurred'; + console.error('Error in API:', errorMessage); + + if (error.code === 'P2002') { + return NextResponse.json( + { error: 'A tool with this name already exists.' }, + { status: 409 } + ); + } + return NextResponse.json( - { error: 'Error processing request. Please try again.' }, + { error: `Error processing request: ${errorMessage}` }, { status: error.response?.status || 500 } ); }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (60)
public/images/img/Airtable.png
is excluded by!**/*.png
public/images/img/Aiven.png
is excluded by!**/*.png
public/images/img/AppoloGraphQL.png
is excluded by!**/*.png
public/images/img/Appsmith.png
is excluded by!**/*.png
public/images/img/Appwrite.png
is excluded by!**/*.png
public/images/img/Cal.png
is excluded by!**/*.png
public/images/img/Clerk.png
is excluded by!**/*.png
public/images/img/Firebase.png
is excluded by!**/*.png
public/images/img/Formbricks.png
is excluded by!**/*.png
public/images/img/HarperDB.png
is excluded by!**/*.png
public/images/img/Hygraph.png
is excluded by!**/*.png
public/images/img/Liveblocks.png
is excluded by!**/*.png
public/images/img/Livekit.png
is excluded by!**/*.png
public/images/img/Medusa.png
is excluded by!**/*.png
public/images/img/MindsDB.png
is excluded by!**/*.png
public/images/img/Neon.png
is excluded by!**/*.png
public/images/img/NocoDB.png
is excluded by!**/*.png
public/images/img/Oneentry.png
is excluded by!**/*.png
public/images/img/Plausible.png
is excluded by!**/*.png
public/images/img/Posthog.png
is excluded by!**/*.png
public/images/img/RisingWave.png
is excluded by!**/*.png
public/images/img/Sanity.png
is excluded by!**/*.png
public/images/img/Shopify.png
is excluded by!**/*.png
public/images/img/Strapi.png
is excluded by!**/*.png
public/images/img/Stream.png
is excluded by!**/*.png
public/images/img/Supabase.png
is excluded by!**/*.png
public/images/img/Twilio.png
is excluded by!**/*.png
public/images/img/Typebot.png
is excluded by!**/*.png
public/images/img/Typeform.png
is excluded by!**/*.png
public/images/img/UseFathom.png
is excluded by!**/*.png
public/images/logo/Airtable.png
is excluded by!**/*.png
public/images/logo/Aiven.png
is excluded by!**/*.png
public/images/logo/AppoloGraphQL.png
is excluded by!**/*.png
public/images/logo/Appsmith.png
is excluded by!**/*.png
public/images/logo/Appwrite.png
is excluded by!**/*.png
public/images/logo/Cal.png
is excluded by!**/*.png
public/images/logo/Clerk.png
is excluded by!**/*.png
public/images/logo/Firebase.png
is excluded by!**/*.png
public/images/logo/Formbricks.png
is excluded by!**/*.png
public/images/logo/HarperDB.png
is excluded by!**/*.png
public/images/logo/Hygraph.png
is excluded by!**/*.png
public/images/logo/Liveblocks.png
is excluded by!**/*.png
public/images/logo/Livekit.png
is excluded by!**/*.png
public/images/logo/Medusa.png
is excluded by!**/*.png
public/images/logo/MindsDB.png
is excluded by!**/*.png
public/images/logo/Neon.png
is excluded by!**/*.png
public/images/logo/NocoDB.png
is excluded by!**/*.png
public/images/logo/Oneentry.png
is excluded by!**/*.png
public/images/logo/Plausible.png
is excluded by!**/*.png
public/images/logo/Posthog.png
is excluded by!**/*.png
public/images/logo/RisingWave.png
is excluded by!**/*.png
public/images/logo/Sanity.png
is excluded by!**/*.png
public/images/logo/Shopify.png
is excluded by!**/*.png
public/images/logo/Strapi.png
is excluded by!**/*.png
public/images/logo/Stream.png
is excluded by!**/*.png
public/images/logo/Supabase.png
is excluded by!**/*.png
public/images/logo/Twilio.png
is excluded by!**/*.png
public/images/logo/Typebot.png
is excluded by!**/*.png
public/images/logo/Typeform.png
is excluded by!**/*.png
public/images/logo/UseFathom.png
is excluded by!**/*.png
📒 Files selected for processing (15)
prisma/migrations/20241214052435_features_added/migration.sql
(1 hunks)prisma/schema.prisma
(1 hunks)src/app/api/tools/route.ts
(1 hunks)src/app/page.tsx
(1 hunks)src/app/tools/[slug]/loading.tsx
(1 hunks)src/components/tools/Categories.tsx
(1 hunks)src/components/tools/LoadingCategories.tsx
(1 hunks)src/components/tools/SearchBar.tsx
(1 hunks)src/components/tools/ToolCard.tsx
(1 hunks)src/components/tools/ToolSkeleton.tsx
(1 hunks)src/components/tools/ToolsFilter.tsx
(1 hunks)src/components/tools/ToolsPage.tsx
(1 hunks)src/components/tools/toolDetails/SimilarTools.tsx
(1 hunks)src/components/tools/toolDetails/ToolDetailsPage.tsx
(1 hunks)src/lib/toolData.json
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
- src/app/tools/[slug]/loading.tsx
- src/components/tools/ToolSkeleton.tsx
- src/components/tools/LoadingCategories.tsx
- src/components/tools/SearchBar.tsx
- src/components/tools/toolDetails/SimilarTools.tsx
- src/components/tools/ToolsPage.tsx
- src/app/page.tsx
🧰 Additional context used
📓 Learnings (2)
prisma/schema.prisma (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#39
File: prisma/schema.prisma:52-54
Timestamp: 2024-11-23T12:12:03.854Z
Learning: In `prisma/schema.prisma`, the `Tool` model and its indexes are temporary placeholders, and the proper database structure will be added later.
src/components/tools/ToolCard.tsx (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#44
File: src/components/tools/ToolCard.tsx:19-23
Timestamp: 2024-11-30T20:29:45.779Z
Learning: In the `src/components/tools/ToolCard.tsx` file, it's acceptable to use `tool.logo` directly in the `img` `src` attribute without additional validation because the image URLs are controlled by the backend, and overengineering is to be avoided before the MVP is complete.
🪛 Biome (1.9.4)
src/app/api/tools/route.ts
[error] 47-47: Useless case clause.
because the default clause is present:
Unsafe fix: Remove the useless case.
(lint/complexity/noUselessSwitchCase)
[error] 7-7: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.
See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.
(lint/suspicious/noGlobalIsNan)
src/components/tools/ToolCard.tsx
[error] 30-38: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (7)
src/components/tools/ToolsFilter.tsx (1)
14-33
: 🛠️ Refactor suggestion
Move categories and tags to a configuration file
Hardcoding these values makes maintenance difficult and could lead to inconsistencies. Consider:
- Moving these arrays to a configuration file
- Adding TypeScript types/enums
- Removing duplicate values between categories and tags (e.g., "api", "devops")
Create a new config file:
// src/config/tools.ts
export type Category = "tools" | "frontend" | "backend" | "api" | "devops" | "cloud" | "e-commerce" | "chat";
export type Tag = "postgreSQL" | "cloud-native" | "NoSQL" | "api" | "GraphQL" | "CMS" | "database" | "devops";
export const TOOL_CATEGORIES: Category[] = ["tools", "frontend", "backend", "api", "devops", "cloud", "e-commerce", "chat"];
export const TOOL_TAGS: Tag[] = ["postgreSQL", "cloud-native", "NoSQL", "api", "GraphQL", "CMS", "database", "devops"];
src/components/tools/Categories.tsx (2)
19-33
: Consider using consistent category styling instead of random colors and icons
The randomization of colors and icons can lead to an inconsistent user experience, with styles changing on each render.
Also applies to: 35-52
73-76
: Fix grammatical error in error message
The error message contains a grammatical error: "We couldn't able to find the categories for you."
74-76
: Handle invalid date formats
This issue was previously identified in past review comments. The suggestion to handle invalid dates is still applicable.
src/components/tools/toolDetails/ToolDetailsPage.tsx (1)
22-34
:
Replace hardcoded language statistics with GitHub API data.
The language statistics and colors are currently hardcoded, which won't reflect the actual repository composition. Consider fetching this data from the GitHub API.
Apply this diff:
-const languageColors: { [key: string]: string } = {
- JavaScript: "#f1e05a",
- TypeScript: "#2b7489",
- CSS: "#563d7c",
- HTML: "#e34c26",
-};
-
-const languages: { [key: string]: number } = {
- JavaScript: 70,
- TypeScript: 20,
- CSS: 5,
- HTML: 5,
-};
+async function getLanguageStats(githubUrl: string) {
+ const [owner, repo] = githubUrl.split('/').slice(-2);
+ const response = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/languages`
+ );
+ const data = await response.json();
+ const total = Object.values(data).reduce((a: any, b) => a + b, 0);
+ return Object.entries(data).map(([name, bytes]) => ({
+ name,
+ percentage: ((bytes as number) / total) * 100,
+ }));
+}
Likely invalid or redundant comment.
src/lib/toolData.json (1)
298-319
:
Add missing categories field.
The Appwrite tool entry is missing the categories
field while having tags
, which could cause inconsistencies in filtering and display.
Add the categories field:
+ "categories": [
+ "open-source",
+ "backend"
+ ],
"tags": [
Likely invalid or redundant comment.
src/app/api/tools/route.ts (1)
6-7
:
Replace unsafe isNaN with Number.isNaN.
Using the global isNaN
function can lead to unexpected results due to type coercion.
🧰 Tools
🪛 Biome (1.9.4)
[error] 7-7: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.
See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.
(lint/suspicious/noGlobalIsNan)
const response = await getToolCategories(); | ||
const variousCategories = response.categories; | ||
|
||
if (!response.status) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance error handling for API response
The current error handling only checks response.status
. Consider adding try-catch and additional error checks.
const Categories = async () => {
- const response = await getToolCategories();
- const variousCategories = response.categories;
- if (!response.status) {
+ try {
+ const response = await getToolCategories();
+ if (!response?.status || !Array.isArray(response?.categories)) {
+ throw new Error('Invalid response format');
+ }
+ const variousCategories = response.categories;
+ } catch (error) {
Committable suggestion skipped: line range outside the PR's diff.
<div className="grid gap-2 md:gap-4 grid-cols-2 lg:grid-cols-4"> | ||
{variousCategories?.map((category) => ( | ||
<Link | ||
key={category.name} | ||
href={`/tools?categories=${category.name}`} | ||
> | ||
<div className="flex items-center space-x-4 p-2 md:p-4 bg-card shadow hover:bg-[#141414] bg-opacity-20 rounded-xl transition-all duration-300 cursor-pointer"> | ||
<div | ||
className={`${getRandomColor()} w-8 h-8 md:h-12 md:w-12 rounded-full flex items-center justify-center text-white`} | ||
> | ||
{getRandomIcon()} | ||
</div> | ||
<div> | ||
<h3 className="text-sm md:text-lg font-semibold"> | ||
{capitalizeFirstLetter(category.name)} | ||
</h3> | ||
<p className="text-xs md:text-sm text-muted-foreground"> | ||
{category.count || 0} Categories | ||
</p> | ||
</div> | ||
</div> | ||
</Link> | ||
))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance accessibility and add loading states
Consider the following improvements:
- Add ARIA labels for better screen reader support
- Include loading states while data is being fetched
- Add error boundaries to handle runtime errors
+ import { Suspense } from 'react';
+ import { ErrorBoundary } from 'react-error-boundary';
+ import LoadingCategories from './LoadingCategories';
<div className="grid gap-2 md:gap-4 grid-cols-2 lg:grid-cols-4">
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
+ <Suspense fallback={<LoadingCategories />}>
{variousCategories?.map((category) => (
<Link
key={category.name}
href={`/tools?categories=${category.name}`}
+ aria-label={`View ${category.name} tools`}
>
Committable suggestion skipped: line range outside the PR's diff.
/* | ||
Warnings: | ||
|
||
- Added the required column `headline` to the `Tool` table without a default value. This is not possible if the table is not empty. | ||
|
||
*/ | ||
-- AlterTable | ||
ALTER TABLE "Tool" ADD COLUMN "features" TEXT[], | ||
ADD COLUMN "headline" TEXT NOT NULL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Migration could fail on existing data
The migration adds a required headline
column without a default value, which will fail if the Tool
table contains existing records. This is risky for production deployments.
Consider these safer approaches:
- Add column as nullable first, then backfill data:
-- Step 1: Add nullable column
ALTER TABLE "Tool" ADD COLUMN "headline" TEXT;
ALTER TABLE "Tool" ADD COLUMN "features" TEXT[];
-- Step 2: Backfill headline data (in separate migration)
-- UPDATE "Tool" SET "headline" = ...;
-- Step 3: Make headline required (in separate migration)
-- ALTER TABLE "Tool" ALTER COLUMN "headline" SET NOT NULL;
- Or provide a default value:
ALTER TABLE "Tool" ADD COLUMN "headline" TEXT NOT NULL DEFAULT 'No headline available';
ALTER TABLE "Tool" ADD COLUMN "features" TEXT[];
{tool?.categories && tool.categories.slice(0, 3).map((category) => ( | ||
<Badge | ||
key={category} | ||
variant="secondary" | ||
className="bg-[#141414] bg-opacity-80 text-xs text-gray-300 hover:bg-[#141414] hover:bg-opacity-100" | ||
> | ||
{category} | ||
</Badge> | ||
))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Use optional chaining as suggested by static analysis
The current implementation could throw an error if categories
is undefined.
- {tool?.categories && tool.categories.slice(0, 3).map((category) => (
+ {tool.categories?.slice(0, 3).map((category) => (
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
{tool?.categories && tool.categories.slice(0, 3).map((category) => ( | |
<Badge | |
key={category} | |
variant="secondary" | |
className="bg-[#141414] bg-opacity-80 text-xs text-gray-300 hover:bg-[#141414] hover:bg-opacity-100" | |
> | |
{category} | |
</Badge> | |
))} | |
{tool.categories?.slice(0, 3).map((category) => ( | |
<Badge | |
key={category} | |
variant="secondary" | |
className="bg-[#141414] bg-opacity-80 text-xs text-gray-300 hover:bg-[#141414] hover:bg-opacity-100" | |
> | |
{category} | |
</Badge> | |
))} |
🧰 Tools
🪛 Biome (1.9.4)
[error] 30-38: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
<img | ||
src={`/images/logo/${tool.name}.png`} | ||
alt={`${tool.name} logo`} | ||
className="w-10 h-10 object-cover border border-white border-opacity-10 rounded" | ||
/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add error handling for tool logo images.
The current implementation doesn't handle cases where the logo image might be missing or have a different filename.
Apply this diff:
-<img
- src={`/images/logo/${tool.name}.png`}
- alt={`${tool.name} logo`}
- className="w-10 h-10 object-cover border border-white border-opacity-10 rounded"
-/>
+<img
+ src={`/images/logo/${tool.name}.png`}
+ alt={`${tool.name} logo`}
+ className="w-10 h-10 object-cover border border-white border-opacity-10 rounded"
+ onError={(e) => {
+ e.currentTarget.src = '/images/default-logo.png';
+ }}
+/>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<img | |
src={`/images/logo/${tool.name}.png`} | |
alt={`${tool.name} logo`} | |
className="w-10 h-10 object-cover border border-white border-opacity-10 rounded" | |
/> | |
<img | |
src={`/images/logo/${tool.name}.png`} | |
alt={`${tool.name} logo`} | |
className="w-10 h-10 object-cover border border-white border-opacity-10 rounded" | |
onError={(e) => { | |
e.currentTarget.src = '/images/default-logo.png'; | |
}} | |
/> |
"stars": 4900, | ||
"forks": "7300", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix inconsistent data type for forks count.
The forks
value is a string instead of a number, which could cause type mismatches when used in the application.
Apply this diff:
- "forks": "7300",
+ "forks": 7300,
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
"stars": 4900, | |
"forks": "7300", | |
"stars": 4900, | |
"forks": 7300, |
const createdTools = await Promise.all( | ||
tools.map(async (tool) => { | ||
return prisma.tool.create({ | ||
data: { | ||
name: tool.name, | ||
description: tool.description, | ||
headline: tool.headline, | ||
features: tool.features, | ||
logo: tool.name, | ||
categories: tool.categories || [], | ||
tags: tool.tags || [], | ||
stars: tool.stars || 0, | ||
forks: tool.forks || 0, | ||
lastUpdated: new Date(), | ||
websiteUrl: tool.websiteUrl, | ||
githubUrl: tool?.githubUrl, | ||
documentation: tool?.documentation | ||
}, | ||
}); | ||
}) | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add batch processing for database operations.
The current implementation processes each tool creation sequentially. Consider using Prisma's createMany
for better performance.
Apply this diff:
- const createdTools = await Promise.all(
- tools.map(async (tool) => {
- return prisma.tool.create({
- data: {
+ const createdTools = await prisma.tool.createMany({
+ data: tools.map((tool) => ({
name: tool.name,
description: tool.description,
headline: tool.headline,
features: tool.features,
logo: tool.name,
categories: tool.categories || [],
tags: tool.tags || [],
stars: tool.stars || 0,
forks: tool.forks || 0,
lastUpdated: new Date(),
websiteUrl: tool.websiteUrl,
githubUrl: tool?.githubUrl,
documentation: tool?.documentation
- },
- });
- })
+ }))
);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
const createdTools = await Promise.all( | |
tools.map(async (tool) => { | |
return prisma.tool.create({ | |
data: { | |
name: tool.name, | |
description: tool.description, | |
headline: tool.headline, | |
features: tool.features, | |
logo: tool.name, | |
categories: tool.categories || [], | |
tags: tool.tags || [], | |
stars: tool.stars || 0, | |
forks: tool.forks || 0, | |
lastUpdated: new Date(), | |
websiteUrl: tool.websiteUrl, | |
githubUrl: tool?.githubUrl, | |
documentation: tool?.documentation | |
}, | |
}); | |
}) | |
); | |
const createdTools = await prisma.tool.createMany({ | |
data: tools.map((tool) => ({ | |
name: tool.name, | |
description: tool.description, | |
headline: tool.headline, | |
features: tool.features, | |
logo: tool.name, | |
categories: tool.categories || [], | |
tags: tool.tags || [], | |
stars: tool.stars || 0, | |
forks: tool.forks || 0, | |
lastUpdated: new Date(), | |
websiteUrl: tool.websiteUrl, | |
githubUrl: tool?.githubUrl, | |
documentation: tool?.documentation | |
})) | |
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Outside diff range and nitpick comments (7)
src/lib/types.ts (3)
1-18
: Consider refactoring interfaces to reduce duplicationThe
ToolCardInterface
andToolDetailsInterface
share many common properties. Consider creating a base interface to reduce duplication and maintain consistency.interface BaseToolInterface { name: string description: string categories: string[] stars: number forks: number headline: string features: string[] logo: string tags: string[] } export interface ToolCardInterface extends BaseToolInterface { id: string lastUpdated?: Date githubUrl?: string websiteUrl?: string documentation?: string createdAt?: Date updatedAt?: Date } export interface ToolDetailsInterface extends BaseToolInterface { id?: string lastUpdated: Date githubUrl: string websiteUrl?: string | null documentation?: string | null createdAt: Date updatedAt: Date }Also applies to: 21-38
34-35
: Standardize nullability patternThe
websiteUrl
anddocumentation
properties usestring | null
inToolDetailsInterface
while other optional properties useundefined
. Consider standardizing the nullability pattern across the interfaces.- websiteUrl?: string | null - documentation?: string | null + websiteUrl?: string + documentation?: string
2-4
: Add validation constraints using JSDocConsider adding JSDoc comments with validation constraints for critical fields like URLs and descriptions.
/** @minLength 1 @maxLength 100 */ name: string /** @minLength 10 @maxLength 500 */ description: string /** @pattern ^https?:// */ githubUrl: stringAlso applies to: 23-24
src/components/tools/ToolCard.tsx (3)
30-38
: Simplify categories rendering with optional chainingThe current implementation has redundant null checking.
- {tool?.categories && tool.categories.slice(0, 3).map((category) => ( + {tool.categories?.slice(0, 3).map((category) => (🧰 Tools
🪛 Biome (1.9.4)
[error] 30-38: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
39-46
: Extract categories limit to a constantThe magic number
3
is used multiple times for category limiting. Consider extracting it to a named constant.+ const MAX_VISIBLE_CATEGORIES = 3; ... - {tool.categories?.slice(0, 3).map((category) => + {tool.categories?.slice(0, MAX_VISIBLE_CATEGORIES).map((category) => ... - {tool.categories && tool.categories.length > 3 && ( + {tool.categories && tool.categories.length > MAX_VISIBLE_CATEGORIES && ( ... - +{tool.categories.length - 3} + +{tool.categories.length - MAX_VISIBLE_CATEGORIES}
52-57
: Add aria-labels for better accessibilityThe star and fork counts should have proper aria-labels for screen readers.
- <div className="flex items-center"> + <div className="flex items-center" aria-label={`${tool.stars.toLocaleString()} stars`}> ... - <div className="flex items-center"> + <div className="flex items-center" aria-label={`${tool.forks.toLocaleString()} forks`}>Also applies to: 61-66
src/components/tools/AlgoliaSearch.tsx (1)
1-41
: Consider enhancing the implementation to fully meet requirements.Based on the PR objectives, this implementation seems incomplete:
- The search functionality is implemented but not integrated with the UI
- The component doesn't handle the filters mentioned in the PR objectives
- There's no error handling or loading states for search operations
Consider:
- Implementing search input and results display
- Adding filter integration
- Including loading and error states
- Adding proper client-side caching for search results
Would you like help implementing these additional features?
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/components/tools/AlgoliaSearch.tsx
(1 hunks)src/components/tools/ToolCard.tsx
(1 hunks)src/lib/types.ts
(1 hunks)
🧰 Additional context used
📓 Learnings (2)
src/components/tools/AlgoliaSearch.tsx (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#44
File: src/components/tools/AlgoliaSearch.tsx:42-49
Timestamp: 2024-11-30T20:14:25.360Z
Learning: The `AlgoliaSearch` component in `src/components/tools/AlgoliaSearch.tsx` currently does not require `searchParams` and does not need to accept them as props.
src/components/tools/ToolCard.tsx (1)
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#44
File: src/components/tools/ToolCard.tsx:19-23
Timestamp: 2024-11-30T20:29:45.779Z
Learning: In the `src/components/tools/ToolCard.tsx` file, it's acceptable to use `tool.logo` directly in the `img` `src` attribute without additional validation because the image URLs are controlled by the backend, and overengineering is to be avoided before the MVP is complete.
🪛 Biome (1.9.4)
src/components/tools/ToolCard.tsx
[error] 30-38: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🔇 Additional comments (6)
src/components/tools/ToolCard.tsx (2)
74-76
: Handle invalid date formats
This is a duplicate of a previous review comment. The date handling needs improvement to handle invalid dates.
17-21
:
Avoid hardcoded image paths
The image path is hardcoded to /images/logo/${tool.name}.png
. This assumes all logos are PNGs and follow a specific naming convention.
Consider:
- Using the
logo
property fromToolCardInterface
which is currently unused - Adding error handling for missing images
- src={`/images/logo/${tool.name}.png`}
+ src={tool.logo}
+ onError={(e) => {
+ e.currentTarget.src = '/images/default-logo.png'
+ }}
⛔ Skipped due to learnings
Learnt from: Ankur1493
PR: tyaga001/devtoolsacademy#44
File: src/components/tools/ToolCard.tsx:19-23
Timestamp: 2024-11-30T20:29:45.779Z
Learning: In the `src/components/tools/ToolCard.tsx` file, it's acceptable to use `tool.logo` directly in the `img` `src` attribute without additional validation because the image URLs are controlled by the backend, and overengineering is to be avoided before the MVP is complete.
src/components/tools/AlgoliaSearch.tsx (4)
1-10
: LGTM! Clean imports and well-structured interface.
The imports are appropriate and the interface is well-defined with proper typing.
17-20
: Ensure environment variables are properly handled
The environment variables are using non-null assertion operators (!
). This should be handled more safely.
22-27
: LGTM! Well-implemented search functionality.
The Algolia search implementation is clean with appropriate pagination (20 items per page) and proper typing.
30-37
:
Remove unnecessary async keyword and enhance component implementation.
The component has several issues:
- It's marked as
async
but contains no async operations - The implementation seems incomplete as it's not utilizing the search functionality from
getTools
Apply this diff to fix the immediate issues:
-const AlgoliaSearch: React.FC<AlgoliaSearchProps> = async () => {
+const AlgoliaSearch: React.FC<AlgoliaSearchProps> = () => {
Let's verify if the ToolsFilter component handles the search functionality:
Features Integrated
This was me creating tool card skeletons and showing the cards design, had to checkout other competitors and create a UI design which suits theme
Created a exploration feature on the below of tools page, which let users checkout the different categories we have stored and number of tools in those categories, UI inspiration was Lu.ma/discover
Why client side when it can be done on the server side, but it would be a way in UX, we can show a proper skeletons here, and decrease the load on the server for fetching these tools, we can integrate react query or something later for caching but yeah, this is what I felt as adding filters sorting would be simpler this way
Filters are based on categories and tags as of now but we can embed more later after tools collection, also added sorting based on the popularity (defined by stars) and recent (defined by last commited)
Used Algolia for searching tools and replacing the tools which were sent by the api routes earlier
this can be improved based on the UI as we can add more details later, also with how many images we want to showcase of the product
In the tool details page added a similar tools feature which showcase tools with similar categories and tags, but this is fetched on the client side as the complete tooldetails page is loaded on the server side, so we can reduce the payload size by calling this on client
Created proper error based skeletons and checks
What can be improved
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Documentation
Chores