Repository: ProgramadoresSemPatria/sem-patria-community
Pull request #197: Add new entities on Search Bar
Author: login: bebossi
Created: 2025-03-08T20:45:10Z | Merged: 2025-03-21T02:08:48Z

Need to be specified better what is the behavior when clicking on event. I will create a ticket on it:
https://www.loom.com/share/caac452467f04df88878c864695dd1d6

Add new entities:
- mentorship
- courses
- calendar (events)
- interests


Diff:
diff --git a/.gitignore b/.gitignore
index 50749f3d..e79c62a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
 /.pnp
 .pnp.js
 .yarn/install-state.gz
+.npm
 
 # testing
 /coverage
@@ -22,6 +23,8 @@
 *.pem
 
 # debug
+logs
+*.log
 npm-debug.log*
 yarn-debug.log*
 yarn-error.log*
@@ -31,6 +34,11 @@ yarn-error.log*
 .env*.local
 .env
 
+# editors
+.idea
+.cursorignore
+.cursorrules
+
 # vercel
 .vercel
 
diff --git a/src/app/(private)/(routes)/(member)/interests/components/interest-explorer.tsx b/src/app/(private)/(routes)/(member)/interests/components/interest-explorer.tsx
index 11a1e7ef..eb861ca8 100644
--- a/src/app/(private)/(routes)/(member)/interests/components/interest-explorer.tsx
+++ b/src/app/(private)/(routes)/(member)/interests/components/interest-explorer.tsx
@@ -1,9 +1,10 @@
 'use client'
 
-import { type FC, useState } from 'react'
+import { type FC, useEffect, useState } from 'react'
 import { type InterestWithUsers } from '../page'
 import InterestItem from './interest-item'
 import InterestModal from './interest-modal'
+import { useRouter, useSearchParams } from 'next/navigation'
 
 interface InterestExplorerProps {
   interests: InterestWithUsers[]
@@ -11,9 +12,23 @@ interface InterestExplorerProps {
 }
 
 const InterestExplorer: FC<InterestExplorerProps> = ({ interests, userId }) => {
+  const router = useRouter()
+  const searchParams = useSearchParams()
   const [selectedInterest, setSelectedInterest] =
     useState<InterestWithUsers | null>(null)
 
+  useEffect(() => {
+    const interestId = searchParams.get('interestId')
+
+    if (interestId) {
+      const foundInterest = interests.find(i => i.id === interestId)
+      if (foundInterest) {
+        setSelectedInterest(foundInterest)
+        router.replace(`/interests`)
+      }
+    }
+  }, [searchParams, interests, router])
+
   const handleInterestClick = (interest: InterestWithUsers) => {
     setSelectedInterest(interest)
   }
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
index 091ee458..f4e4d30b 100644
--- a/src/app/api/search/route.ts
+++ b/src/app/api/search/route.ts
@@ -1,3 +1,4 @@
+import { type Entity } from '@/hooks/search/types'
 import prismadb from '@/lib/prismadb'
 import { auth } from '@clerk/nextjs/server'
 import { type NextRequest, NextResponse } from 'next/server'
@@ -9,77 +10,123 @@ export async function POST(request: NextRequest) {
       return new NextResponse('Unauthenticated', { status: 401 })
     }
 
-    const { searchParams } = request.nextUrl
-    const keyword = searchParams.get('keyword')
-
+    const keyword = request.nextUrl.searchParams.get('keyword')
     if (!keyword) {
       return new NextResponse('Keyword is required', { status: 400 })
     }
 
-    const queryPosts = prismadb.post.findMany({
-      where: {
-        title: {
-          contains: keyword,
-          mode: 'insensitive'
-        }
-      },
-      orderBy: {
-        createdAt: 'desc'
-      },
-      select: {
-        id: true,
-        title: true,
-        createdAt: true,
-        category: {
+    const [posts, users, classrooms, courses, interests, events] =
+      await prismadb.$transaction([
+        prismadb.post.findMany({
+          where: { title: { contains: keyword, mode: 'insensitive' } },
+          orderBy: { createdAt: 'desc' },
           select: {
-            name: true
+            id: true,
+            title: true,
+            createdAt: true,
+            category: { select: { name: true } },
+            user: { select: { username: true } },
+            _count: { select: { likes: true } }
           }
-        },
-        user: {
+        }),
+
+        prismadb.user.findMany({
+          where: { username: { contains: keyword, mode: 'insensitive' } },
           select: {
-            username: true
+            id: true,
+            username: true,
+            name: true,
+            createdAt: true,
+            imageUrl: true,
+            followers: true
           }
-        },
-        _count: {
-          select: { likes: true }
-        }
-      }
-    })
+        }),
 
-    const queryUsers = prismadb.user.findMany({
-      where: {
-        username: {
-          contains: keyword,
-          mode: 'insensitive'
-        }
-      },
-      select: {
-        id: true,
-        username: true,
-        name: true,
-        createdAt: true,
-        imageUrl: true,
-        followers: true
-      }
-    })
+        prismadb.classroom.findMany({
+          where: { title: { contains: keyword, mode: 'insensitive' } },
+          select: {
+            id: true,
+            title: true,
+            createdAt: true,
+            modules: {
+              select: {
+                videos: { select: { id: true } }
+              }
+            }
+          }
+        }),
+
+        prismadb.course.findMany({
+          where: { name: { contains: keyword, mode: 'insensitive' } },
+          select: {
+            id: true,
+            name: true,
+            createdAt: true,
+            isPaid: true,
+            level: true,
+            courseUrl: true,
+            category: { select: { name: true } }
+          }
+        }),
+
+        prismadb.interest.findMany({
+          where: { interest: { contains: keyword, mode: 'insensitive' } },
+          select: {
+            id: true,
+            interest: true,
+            createdAt: true,
+            users: { select: { username: true } }
+          }
+        }),
+
+        prismadb.event.findMany({
+          where: { title: { contains: keyword, mode: 'insensitive' } },
+          select: {
+            id: true,
+            title: true,
+            date: true,
+            location: true,
+            externalUrl: true,
+            specialGuest: true
+          }
+        })
+      ])
 
-    const [posts, users] = await prismadb.$transaction([queryPosts, queryUsers])
+    const addEntityType = <T>(items: T[], entity: Entity) =>
+      items.map(item => ({ ...item, entity }))
 
+    const results = [
+      ...addEntityType(posts, 'forum'),
+      ...addEntityType(users, 'user'),
+      ...addEntityType(classrooms, 'classroom'),
+      ...addEntityType(courses, 'course'),
+      ...addEntityType(interests, 'interest'),
+      ...addEntityType(events, 'event')
+    ]
     return NextResponse.json({
-      data: {
-        items: [...posts, ...users],
-        counts: {
-          posts: posts.length,
-          users: users.length
-        }
+      data: { items: results },
+      counts: {
+        posts: posts.length,
+        users: users.length,
+        classrooms: classrooms.length,
+        courses: courses.length,
+        interests: interests.length,
+        events: events.length
       },
       meta: {
         keyword,
         searchedAt: new Date().toISOString(),
-        totalRecords: posts.length + users.length
+        totalRecords: results.length
       }
     })
   } catch (error) {
-    return new NextResponse('Error searching for keyword', { status: 500 })
+    console.error('[SEARCH_API_ERROR]', error)
+    return new NextResponse(
+      JSON.stringify({
+        error: 'Error searching for keyword',
+        details: (error as Error).message
+      }),
+      { status: 500 }
+    )
   }
 }
diff --git a/src/components/events-calendar/index.tsx b/src/components/events-calendar/index.tsx
index 3a734356..b9f57212 100644
--- a/src/components/events-calendar/index.tsx
+++ b/src/components/events-calendar/index.tsx
@@ -20,15 +20,21 @@ import { type EventApiProps } from '@/hooks/event/types'
 import { useAllEvents, useWeekEvents } from '@/hooks/event/use-event'
 import { cn } from '@/lib/utils'
 import { format } from 'date-fns'
-import { useCallback, useMemo, useState } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
 import { EventComponent } from './event-component'
 import { EventsTitle } from './events-title'
 
 type EventsCalendarProps = {
   isWidget?: boolean
+  initialDate?: string
+  initialEventId?: string
 }
 
-export const EventsCalendar = ({ isWidget = false }: EventsCalendarProps) => {
+export const EventsCalendar = ({
+  isWidget = false,
+  initialDate,
+  initialEventId
+}: EventsCalendarProps) => {
   const { data, isLoading } = useWeekEvents()
   const { data: allEvents } = useAllEvents()
 
@@ -76,6 +82,15 @@ export const EventsCalendar = ({ isWidget = false }: EventsCalendarProps) => {
     [allEvents]
   )
 
+  useEffect(() => {
+    if (initialEventId && allEvents) {
+      const event = allEvents.find(e => e.id === initialEventId)
+      if (event) {
+        handleClickDay(new Date(event.date))
+      }
+    }
+  }, [initialDate, initialEventId, allEvents, handleClickDay])
+
   if (isWidget) {
     return (
       <div className="flex flex-col gap-y-2">
@@ -152,7 +167,7 @@ export const EventsCalendar = ({ isWidget = false }: EventsCalendarProps) => {
           </div>
         </CardTitle>
         <CardDescription>
-          Plan your time and don&apos;t miss any event
+          Plan your time and don&apos;t miss any event.
         </CardDescription>
       </CardHeader>
       <CardContent className="flex flex-col gap-2 md:gap-0 md:flex-row justify-start items-start p-4 h-full">
@@ -161,6 +176,9 @@ export const EventsCalendar = ({ isWidget = false }: EventsCalendarProps) => {
           selected={allEvents?.map(d => new Date(d.date))}
           className="rounded-md border w-fit"
           onDayClick={handleClickDay}
+          month={
+            typeof initialDate === 'string' ? new Date(initialDate) : undefined
+          }
         />
 
         <section className="flex flex-col gap-4 pl-4 overflow-y-auto max-h-72 max-w-sm">
diff --git a/src/components/modals/events-modal/index.tsx b/src/components/modals/events-modal/index.tsx
index 143cb9d3..4e5059f3 100644
--- a/src/components/modals/events-modal/index.tsx
+++ b/src/components/modals/events-modal/index.tsx
@@ -4,21 +4,22 @@ import { Separator } from '@/components/ui/separator'
 import { useEventModal } from '@/hooks/modal/use-event-modal'
 
 const EventsModal = () => {
-  const { isOpen, onClose } = useEventModal()
+  const { isOpen, onClose, initialDate, initialEventId } = useEventModal()
 
   return (
-    <>
-      <Modal
-        title="Mentorship Events"
-        description="Events that are happening in the mentorship program."
-        isOpen={isOpen}
-        onClose={onClose}
-        className="max-w-fit"
-      >
-        <Separator className="mb-4" />
-        <EventsCalendar />
-      </Modal>
-    </>
+    <Modal
+      title="Mentorship Events"
+      description="Events that are happening in the mentorship program."
+      isOpen={isOpen}
+      onClose={onClose}
+      className="max-w-fit"
+    >
+      <Separator className="mb-4" />
+      <EventsCalendar
+        initialDate={initialDate}
+        initialEventId={initialEventId}
+      />
+    </Modal>
   )
 }
 
diff --git a/src/components/top-bar/events-button.tsx b/src/components/top-bar/events-button.tsx
index eaa19f84..35080ab4 100644
--- a/src/components/top-bar/events-button.tsx
+++ b/src/components/top-bar/events-button.tsx
@@ -21,7 +21,9 @@ export const EventsButton = ({ isMobile }: EventsButtonProps) => {
 
   return (
     <Button
-      onClick={onOpen}
+      onClick={() => {
+        onOpen()
+      }}
       variant="ghost"
       size="icon"
       className={isMobile ? 'mr-4' : undefined}
diff --git a/src/components/top-bar/search-dialog/ResultComponents.tsx b/src/components/top-bar/search-dialog/ResultComponents.tsx
new file mode 100644
index 00000000..aded29c9
--- /dev/null
+++ b/src/components/top-bar/search-dialog/ResultComponents.tsx
@@ -0,0 +1,187 @@
+import { Icons } from '@/components/icons'
+import Image from 'next/image'
+import { type SearchDialogResult } from '@/hooks/search/types'
+
+interface ResultProps {
+  result: SearchDialogResult
+}
+
+export const ForumResult = ({ result }: ResultProps) => (
+  <>
+    <div className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-indigo-500">
+      <Icons.forum className="w-6 h-6 text-white" aria-hidden="true" />
+    </div>
+    <div className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.title}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs text-gray-500 dark:text-gray-400">
+        <p className="truncate">
+          Posted by <strong>@{result.user?.username}</strong> in{' '}
+          <strong>{result.category?.name}</strong>
+        </p>
+        {result._count?.likes && result._count?.likes > 0 && (
+          <>
+            <svg viewBox="0 0 2 2" className="w-0.5 h-0.5 fill-current">
+              <circle r={1} cx={1} cy={1} />
+            </svg>
+            <div className="flex items-center gap-x-1">
+              <Icons.upVote className="w-3.5 h-3.5" />
+              {result._count?.likes}
+            </div>
+          </>
+        )}
+      </div>
+    </div>
+  </>
+)
+
+export const UserResult = ({ result }: ResultProps) => (
+  <>
+    <div className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-blue-500">
+      {result.imageUrl ? (
+        <Image
+          alt="User avatar"
+          className="rounded-lg"
+          width={500}
+          height={500}
+          src={result.imageUrl}
+        />
+      ) : (
+        <Icons.user className="w-6 h-6 text-white" aria-hidden="true" />
+      )}
+    </div>
+    <div className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.name}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs text-gray-500 dark:text-gray-400">
+        <p className="truncate">
+          <strong>@{result.username}</strong>
+        </p>
+        <svg viewBox="0 0 2 2" className="w-0.5 h-0.5 fill-current">
+          <circle r={1} cx={1} cy={1} />
+        </svg>
+        <p className="truncate">{result.followers} followers</p>
+      </div>
+    </div>
+  </>
+)
+
+const ClassroomResult = ({ result }: ResultProps) => (
+  <>
+    <div
+      id="entity-icon"
+      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-green-500"
+    >
+      <Icons.classroom className="w-6 h-6 text-white" aria-hidden="true" />
+    </div>
+    <div id="title" className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.title}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
+        {result.modules && result.modules.length > 0 && (
+          <div className="flex items-center gap-x-1 text-xs/5 text-gray-500 dark:text-gray-400">
+            <Icons.book className="w-3.5 h-3.5" /> {result.modules?.length}{' '}
+            modules
+          </div>
+        )}
+      </div>
+    </div>
+  </>
+)
+
+const CourseResult = ({ result }: ResultProps) => (
+  <>
+    <div
+      id="entity-icon"
+      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-yellow-500"
+    >
+      <Icons.code className="w-6 h-6 text-white" aria-hidden="true" />
+    </div>
+    <div id="title" className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.name}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
+        <p className="truncate">
+          Category: <strong>{result.category?.name}</strong>
+        </p>
+        <svg
+          viewBox="0 0 2 2"
+          className="flex-shrink-0 w-0.5 h-0.5 fill-current"
+        >
+          <circle r={1} cx={1} cy={1} />
+        </svg>
+        <p className="truncate">{result.isPaid ? 'Paid' : 'Free'} Course</p>
+      </div>
+    </div>
+  </>
+)
+
+const InterestResult = ({ result }: ResultProps) => (
+  <>
+    <div
+      id="entity-icon"
+      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-primary"
+    >
+      <Icons.list className="w-6 h-6 text-white" aria-hidden="true" />
+    </div>
+    <div id="title" className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.interest}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
+        <p className="truncate">
+          <strong>{result.users?.length || 0} users</strong>
+        </p>
+      </div>
+    </div>
+  </>
+)
+
+const EventResult = ({ result }: ResultProps) => (
+  <>
+    <div
+      id="entity-icon"
+      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-sky-500"
+    >
+      <Icons.calendar className="w-6 h-6 text-white" aria-hidden="true" />
+    </div>
+    <div id="title" className="min-w-0 flex-auto overflow-hidden">
+      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
+        {result.title}
+      </p>
+      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
+        <p className="truncate">
+          <strong>{result.location}</strong>
+        </p>
+        {result.specialGuest && (
+          <>
+            <svg
+              viewBox="0 0 2 2"
+              className="flex-shrink-0 w-0.5 h-0.5 fill-current"
+            >
+              <circle r={1} cx={1} cy={1} />
+            </svg>
+            <p className="truncate">
+              Special Guest: <strong>{result.specialGuest}</strong>
+            </p>
+          </>
+        )}
+      </div>
+    </div>
+  </>
+)
+type ResultComponentMap = {
+  [K in SearchDialogResult['entity']]: React.FC<{ result: SearchDialogResult }>
+}
+export const resultComponents: ResultComponentMap = {
+  forum: ForumResult,
+  user: UserResult,
+  classroom: ClassroomResult,
+  course: CourseResult,
+  interest: InterestResult,
+  event: EventResult
+}
diff --git a/src/components/top-bar/search-dialog/index.tsx b/src/components/top-bar/search-dialog/index.tsx
index 09501481..f00bcc67 100644
--- a/src/components/top-bar/search-dialog/index.tsx
+++ b/src/components/top-bar/search-dialog/index.tsx
@@ -10,6 +10,7 @@ import useSearch from '@/hooks/search/use-search'
 import { type SearchDialogResult } from '@/hooks/search/types'
 import { SearchInput } from './search-input'
 import { SearchResults } from './search-results'
+import { useEventModal } from '@/hooks/modal/use-event-modal'
 
 interface SearchDialogProps {
   isOpen: boolean
@@ -19,6 +20,8 @@ interface SearchDialogProps {
 
 const SearchDialog: React.FC<SearchDialogProps> = ({ isOpen, onClose }) => {
   const router = useRouter()
+  const { onOpen } = useEventModal()
+
   const { isLoading, searchResults, setSearchResults, handleSearch } =
     useSearch()
 
@@ -58,7 +61,12 @@ const SearchDialog: React.FC<SearchDialogProps> = ({ isOpen, onClose }) => {
         >
           <Combobox
             onChange={(item: SearchDialogResult) => {
-              if (item) {
+              if (!item) return
+              if (item.entity === 'event') {
+                handleClose()
+                onOpen(item.date, item.id)
+              }
+              if (item.url) {
                 router.push(item.url)
                 handleClose()
               }
diff --git a/src/components/top-bar/search-dialog/results-list.tsx b/src/components/top-bar/search-dialog/results-list.tsx
index 60ce8d0e..063a6cbe 100644
--- a/src/components/top-bar/search-dialog/results-list.tsx
+++ b/src/components/top-bar/search-dialog/results-list.tsx
@@ -2,103 +2,13 @@ import { Icons } from '@/components/icons'
 import { type SearchDialogResult } from '@/hooks/search/types'
 import { cn } from '@/lib/utils'
 import { ComboboxOption, ComboboxOptions } from '@headlessui/react'
-import { format } from 'date-fns'
-import Image from 'next/image'
+import { formatDateText, getEntityColorClass } from './utils'
+import { resultComponents } from './ResultComponents'
 
 interface ResultsListProps {
   searchResults: SearchDialogResult[]
 }
 
-interface ResultProps {
-  result: SearchDialogResult
-}
-
-const ForumResult = ({ result }: ResultProps) => (
-  <>
-    <div
-      id="entity-icon"
-      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-indigo-500"
-    >
-      <Icons.forum className="w-6 h-6 text-white" aria-hidden="true" />
-    </div>
-    <div id="title" className="min-w-0 flex-auto overflow-hidden">
-      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
-        {result.title}
-      </p>
-      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
-        <p className="truncate">
-          Posted by <strong>@{result.user?.username}</strong> in{' '}
-          <strong>{result.category?.name}</strong>
-        </p>
-        {!result._count?.likes ||
-          (result._count?.likes > 0 && (
-            <>
-              <svg
-                viewBox="0 0 2 2"
-                className="flex-shrink-0 w-0.5 h-0.5 fill-current"
-              >
-                <circle r={1} cx={1} cy={1} />
-              </svg>
-              <div className="flex items-center gap-x-1 text-xs/5 text-gray-500 dark:text-gray-400">
-                <Icons.upVote className="w-3.5 h-3.5" />
-                {result._count?.likes}
-              </div>
-            </>
-          ))}
-      </div>
-    </div>
-  </>
-)
-
-const UserResult = ({ result }: ResultProps) => (
-  <>
-    <div
-      id="entity-icon"
-      className="flex flex-shrink-0 items-center justify-center rounded-lg mr-2 w-12 h-12 bg-purple-500"
-    >
-      {result.imageUrl ? (
-        <Image
-          alt="User avatar"
-          className="rounded-lg"
-          width={500}
-          height={500}
-          src={result.imageUrl}
-        />
-      ) : (
-        <Icons.user className="w-6 h-6 text-white" aria-hidden="true" />
-      )}
-    </div>
-    <div id="title" className="min-w-0 flex-auto overflow-hidden">
-      <p className="truncate capitalize font-medium text-gray-900 dark:text-gray-100">
-        {result.name}
-      </p>
-      <div className="mt-0.5 flex items-center gap-x-2 text-xs/5 text-gray-500 dark:text-gray-400">
-        <p className="truncate">
-          <strong>@{result.username}</strong>
-        </p>
-        <svg
-          viewBox="0 0 2 2"
-          className="flex-shrink-0 w-0.5 h-0.5 fill-current"
-        >
-          <circle r={1} cx={1} cy={1} />
-        </svg>
-        <p className="truncate text-xs/5 text-gray-500 dark:text-gray-400">
-          {result.followers} followers
-        </p>
-      </div>
-    </div>
-  </>
-)
-
-type ResultComponentMap = {
-  [K in SearchDialogResult['entity']]: React.FC<{ result: SearchDialogResult }>
-}
-
-const resultComponents: ResultComponentMap = {
-  forum: ForumResult,
-  user: UserResult
-}
-
 export const ResultsList = ({ searchResults }: ResultsListProps) => (
   <ComboboxOptions
     static
@@ -108,6 +18,7 @@ export const ResultsList = ({ searchResults }: ResultsListProps) => (
     {searchResults.map(sResult => {
       const ResultComponent =
         resultComponents[sResult.entity as keyof typeof resultComponents]
+
       return (
         <ComboboxOption
           key={sResult.id}
@@ -115,11 +26,11 @@ export const ResultsList = ({ searchResults }: ResultsListProps) => (
           as="li"
           className="flex justify-between rounded-lg cursor-pointer select-none items-center px-3 py-2 hover:bg-gray-100 dark:hover:bg-muted transition-colors duration-150"
         >
-          <div className="flex gap-x-4 overflow-hidden">
+          <div className="flex gap-x-2 overflow-hidden">
             <ResultComponent result={sResult} />
           </div>
 
-          <div className="flex shrink-0 items-center gap-x-4">
+          <div className="flex shrink-0 items-center gap-x-2">
             <div className="hidden sm:flex sm:flex-col sm:items-end">
               <span
                 className={cn(
@@ -132,10 +43,7 @@ export const ResultsList = ({ searchResults }: ResultsListProps) => (
                   aria-hidden="true"
                   className={cn(
                     'w-1.5 h-1.5 flex-shrink-0',
-                    sResult.entity === 'forum' &&
-                      'fill-indigo-500 dark:fill-indigo-400',
-                    sResult.entity === 'user' &&
-                      'fill-blue-500 dark:fill-blue-400'
+                    getEntityColorClass(sResult.entity)
                   )}
                 >
                   <circle r={3} cx={3} cy={3} />
@@ -144,8 +52,7 @@ export const ResultsList = ({ searchResults }: ResultsListProps) => (
               </span>
 
               <p className="mt-1 text-xs/5 text-gray-500 dark:text-gray-400">
-                {sResult.entity === 'forum' ? 'Posted' : 'Joined'} on{' '}
-                {format(new Date(sResult.createdAt), 'MMM dd, yyyy')}
+                {formatDateText(sResult.entity, sResult.createdAt)}
               </p>
             </div>
 
diff --git a/src/components/top-bar/search-dialog/utils.ts b/src/components/top-bar/search-dialog/utils.ts
new file mode 100644
index 00000000..cb1ce6bf
--- /dev/null
+++ b/src/components/top-bar/search-dialog/utils.ts
@@ -0,0 +1,87 @@
+import { type SearchDialogResult } from '@/hooks/search/types'
+import { format } from 'date-fns'
+
+export const formatDateText = (entity: string, createdAt: string) => {
+  if (!createdAt) return ''
+
+  let label = ''
+  switch (entity) {
+    case 'user':
+      label = 'Joined'
+      break
+    case 'forum':
+      label = 'Posted'
+      break
+    case 'event':
+      label = 'Hosted'
+      break
+    default:
+      label = 'Created'
+  }
+  return `${label} on ${format(new Date(createdAt), 'MMM dd, yyyy')}`
+}
+
+export const getEntityColorClass = (entity: string) => {
+  const entityColors = {
+    forum: 'fill-indigo-500 dark:fill-indigo-400',
+    user: 'fill-blue-500 dark:fill-blue-400',
+    course: 'fill-yellow-500 dark:fill-yellow-400',
+    classroom: 'fill-green-500 dark:fill-green-400',
+    interest: 'fill-primary dark:fill-primary',
+    event: 'fill-sky-500 dark:fill-sky-400'
+  }
+
+  return entityColors[entity as keyof typeof entityColors] || ''
+}
+export const getItemUrl = (item: SearchDialogResult): string | null => {
+  if (!item) return null
+
+  if (
+    item.entity === 'classroom' &&
+    (!Array.isArray(item.modules) ||
+      !item.modules.length ||
+      !Array.isArray(item.modules[0].videos) ||
+      !item.modules[0].videos.length)
+  ) {
+    return null
+  }
+
+  switch (item.entity) {
+    case 'forum': {
+      const { id, title } = item
+      if (!id || !title) return null
+      const formattedTitle = encodeURIComponent(
+        title.toLowerCase().replace(/\s+/g, '-')
+      )
+      return `/forum/${id}/${formattedTitle}`
+    }
+
+    case 'user': {
+      if (!item.username) return null
+      return `/user/${encodeURIComponent(item.username)}`
+    }
+
+    case 'classroom': {
+      const videoId = item.modules?.[0].videos?.[0]?.id
+      if (!videoId) return null
+      return `/mentorship/${encodeURIComponent(videoId)}`
+    }
+
+    case 'course': {
+      const category = item.category?.name
+        ? encodeURIComponent(item.category.name)
+        : 'all'
+      const level = item.level ? encodeURIComponent(item.level) : 'all'
+      const availability = item.isPaid ? '' : 'free'
+      return `/courses/?category=${category}&level=${level}&availability=${availability}`
+    }
+
+    case 'interest': {
+      if (!item.id) return null
+      return `/interests?interestId=${encodeURIComponent(item.id)}`
+    }
+
+    default:
+      return null
+  }
+}
diff --git a/src/hooks/modal/use-event-modal.tsx b/src/hooks/modal/use-event-modal.tsx
index dc9c505f..b7fc26d6 100644
--- a/src/hooks/modal/use-event-modal.tsx
+++ b/src/hooks/modal/use-event-modal.tsx
@@ -2,16 +2,20 @@ import { create } from 'zustand'
 
 type useEventModalProps = {
   isOpen: boolean
-  onOpen: () => void
+  onOpen: (date?: string, eventId?: string) => void
   onClose: () => void
+  initialDate?: string
+  initialEventId?: string
 }
 
 export const useEventModal = create<useEventModalProps>(set => ({
   isOpen: false,
-  onOpen: () => {
-    set({ isOpen: true })
+  onOpen: (date?: string, eventId?: string) => {
+    set({ isOpen: true, initialDate: date, initialEventId: eventId })
   },
   onClose: () => {
-    set({ isOpen: false })
-  }
+    set({ isOpen: false, initialDate: undefined, initialEventId: undefined })
+  },
+  initialDate: undefined,
+  initialEventId: undefined
 }))
diff --git a/src/hooks/search/types.ts b/src/hooks/search/types.ts
index c655c601..1bb072b6 100644
--- a/src/hooks/search/types.ts
+++ b/src/hooks/search/types.ts
@@ -1,15 +1,36 @@
-export type Entity = 'forum' | 'user'
+export type Entity =
+  | 'forum'
+  | 'user'
+  | 'classroom'
+  | 'course'
+  | 'interest'
+  | 'event'
 
 export type SearchDialogResult = {
   id: string
   entity: Entity
-  url: string
+  url?: string | null
+  date?: string
   createdAt: string
   title?: string
+  courseUrl?: string
+  level?: string
   name?: string
+  location?: string
+  specialGuest?: string
+  interest?: string
+  isPaid?: boolean
   username?: string
   imageUrl?: string
   followers?: string
+  users?: Array<{
+    username: string
+  }>
+  modules?: Array<{
+    videos: Array<{
+      id: string
+    }>
+  }>
   category?: {
     name: string
   }
@@ -27,6 +48,8 @@ export type SearchApiResponse = {
     counts: {
       posts: number
       users: number
+      classrooms: number
+      courses: number
     }
   }
   meta: {
diff --git a/src/hooks/search/use-search.ts b/src/hooks/search/use-search.ts
index 572e421d..72a0c1ec 100644
--- a/src/hooks/search/use-search.ts
+++ b/src/hooks/search/use-search.ts
@@ -1,5 +1,6 @@
 import { useState, useCallback } from 'react'
 import { type SearchDialogResult, type SearchApiResponse } from './types'
+import { getItemUrl } from '@/components/top-bar/search-dialog/utils'
 
 const useSearch = () => {
   const [searchResults, setSearchResults] = useState<SearchDialogResult[]>([])
@@ -29,12 +30,7 @@ const useSearch = () => {
         item => {
           return {
             ...item,
-            entity: item.title ? 'forum' : 'user',
-            url: item.title
-              ? `/forum/${item.id}/${item.title
-                  .toLowerCase()
-                  .replace(/\s+/g, '-')}`
-              : `/user/${item.username}`
+            url: getItemUrl(item)
           }
         }
       )


Reviews:
user: login: iguit0
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T08:35:01Z
user: login: iguit0
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T02:08:29Z

Review comments:
diff_hunk: @@ -1,6 +1,24 @@
 import { useState, useCallback } from 'react'
 import { type SearchDialogResult, type SearchApiResponse } from './types'
 
+function getItemUrl(item: SearchDialogResult): string {
+  console.log('item', item.modules)
path: src/hooks/search/use-search.ts
user: login: iguit0
body: Remove console.log
created_at: 2025-03-11T08:21:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,24 @@
 import { useState, useCallback } from 'react'
 import { type SearchDialogResult, type SearchApiResponse } from './types'
 
+function getItemUrl(item: SearchDialogResult): string {
path: src/hooks/search/use-search.ts
user: login: iguit0
body: I liked the idea to put all together in one place but try to use `const` in utilitary methods to follow the project's pattern.
Example: `src/lib/utils.ts`
created_at: 2025-03-11T08:23:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,6 +1,24 @@
 import { useState, useCallback } from 'react'
 import { type SearchDialogResult, type SearchApiResponse } from './types'
 
+function getItemUrl(item: SearchDialogResult): string {
+  console.log('item', item.modules)
+  switch (item.entity) {
+    case 'forum':
+      return `/forum/${item.id}/${item.title
+        ?.toLowerCase()
+        .replace(/\s+/g, '-')}`
+    case 'user':
+      return `/user/${item.username}`
+    case 'classroom':
+      return `/mentorship/${item.modules?.[0].videos?.[0].id}`
path: src/hooks/search/use-search.ts
user: login: iguit0
body: Please consider adding early validation checks at the beginning of this method (before the switch statement) to ensure `item.modules` exists and has at least one element. Without this, accessing `item.modules[0]` could lead to runtime errors (e.g., undefined access) if `modules` is missing or empty, which might cause unexpected behavior or crashes. For example, you could add a guard clause like `if (!item.modules?.length) return null;` to handle this safely.
created_at: 2025-03-11T08:26:37Z
author_association: CONTRIBUTOR
path: src/components/top-bar/search-dialog/utils.tsx
user: login: iguit0
body: Perhaps you can use as `.ts` 🤔 
Try renaming to `utils.ts`
created_at: 2025-03-11T08:28:00Z
author_association: CONTRIBUTOR
path: src/components/top-bar/search-dialog/results-list.tsx
user: login: iguit0
body: This component has grown excessively long and is taking on too many responsibilities, which makes it harder to maintain and test. To improve readability and adhere to the Single Responsibility Principle, consider extracting specific logic—such as `ClassroomResult` and similar sections—into separate, focused components. This will help break down the complexity and improve modularity.
created_at: 2025-03-11T08:30:12Z
author_association: CONTRIBUTOR
path: src/app/api/search/route.ts
user: login: iguit0
body: If possible, stick with `$transaction` over multiple `findMany` calls. It ensures consistent reads when data might change mid-query, simplifies error handling, and can be more efficient by grouping operations. Multiple calls risk inconsistency and fragmented error management.

[Reference](https://www.prisma.io/docs/orm/prisma-client/queries/transactions#the-transaction-api)
created_at: 2025-03-11T08:34:49Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-03-08T20:45:14Z
body: [vc]: #B+wEMbdU/EUtvCp29W7zwHIUgy/cUAXG+xJ0MwA1XBY=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJib3JkZXJsZXNzLWNvbW11bml0eSIsInJvb3REaXJlY3RvcnkiOm51bGwsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJib3JkZXJsZXNzLWNvbW11bml0eS1naXQtc2VhcmNoLWVuZ2luZS1ib3JkZXJsZXNzLWNvZGluZy52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9ib3JkZXJsZXNzLWNvZGluZy9ib3JkZXJsZXNzLWNvbW11bml0eS8ycVpSN3B2R3BNZlF3Mzh3MmRQQjFudnFldTFoIiwicHJldmlld1VybCI6ImJvcmRlcmxlc3MtY29tbXVuaXR5LWdpdC1zZWFyY2gtZW5naW5lLWJvcmRlcmxlc3MtY29kaW5nLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJib3JkZXJsZXNzLWNvbW11bml0eS1kZXYiLCJyb290RGlyZWN0b3J5IjpudWxsLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiYm9yZGVybGVzcy1jb21tdW5pdHktZGV2LWdpdC1zZWFyY2gtZW5naW5lLWJvcmRlcmxlc3MtY29kaW5nLnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2JvcmRlcmxlc3MtY29kaW5nL2JvcmRlcmxlc3MtY29tbXVuaXR5LWRldi9DVHdiQnU3TnY1MzFvRnNWNTQ2R1Q5cGYxQ3ZiIiwicHJldmlld1VybCI6ImJvcmRlcmxlc3MtY29tbXVuaXR5LWRldi1naXQtc2VhcmNoLWVuZ2luZS1ib3JkZXJsZXNzLWNvZGluZy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn0seyJuYW1lIjoic2VtLXBhdHJpYS1jb21tdW5pdHktcWEiLCJyb290RGlyZWN0b3J5IjpudWxsLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoic2VtLXBhdHJpYS1jb21tdW5pdHktcWEtZ2l0LXNlYXJjaC1lbmdpbmUtYm9yZGVybGVzcy1jb2RpbmcudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vYm9yZGVybGVzcy1jb2Rpbmcvc2VtLXBhdHJpYS1jb21tdW5pdHktcWEvWHlTbnhmWERmVlBKY3lINEhvOW9iek5nZ3poZCIsInByZXZpZXdVcmwiOiJzZW0tcGF0cmlhLWNvbW11bml0eS1xYS1naXQtc2VhcmNoLWVuZ2luZS1ib3JkZXJsZXNzLWNvZGluZy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **borderless-community** | ✅ Ready ([Inspect](https://vercel.com/borderless-coding/borderless-community/2qZR7pvGpMfQw38w2dPB1nvqeu1h)) | [Visit Preview](https://borderless-community-git-search-engine-borderless-coding.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/borderless-community-git-search-engine-borderless-coding.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 2:05am |
| **borderless-community-dev** | ✅ Ready ([Inspect](https://vercel.com/borderless-coding/borderless-community-dev/CTwbBu7Nv531oFsV546GT9pf1Cvb)) | [Visit Preview](https://borderless-community-dev-git-search-engine-borderless-coding.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/borderless-community-dev-git-search-engine-borderless-coding.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 2:05am |
| **sem-patria-community-qa** | ✅ Ready ([Inspect](https://vercel.com/borderless-coding/sem-patria-community-qa/XySnxfXDfVPJcyH4Ho9obzNggzhd)) | [Visit Preview](https://sem-patria-community-qa-git-search-engine-borderless-coding.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sem-patria-community-qa-git-search-engine-borderless-coding.vercel.app?via=pr-comment-feedback-link) | Mar 21, 2025 2:05am |




author_association: NONE
user: login: yuripramos
created_at: 2025-03-19T15:23:02Z
body: @iguit0 Can you review it locally? 

@bebossi We still have the deployment failing =(

author_association: CONTRIBUTOR