diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py
index 2afd99e4..700ae7f6 100644
--- a/apps/api/src/routers/courses/courses.py
+++ b/apps/api/src/routers/courses/courses.py
@@ -24,6 +24,7 @@ from src.services.courses.courses import (
update_course,
delete_course,
update_course_thumbnail,
+ search_courses,
)
from src.services.courses.updates import (
create_update,
@@ -146,6 +147,24 @@ async def api_get_course_by_orgslug(
)
+@router.get("/org_slug/{org_slug}/search")
+async def api_search_courses(
+ request: Request,
+ org_slug: str,
+ query: str,
+ page: int = 1,
+ limit: int = 10,
+ db_session: Session = Depends(get_db_session),
+ current_user: PublicUser = Depends(get_current_user),
+) -> List[CourseRead]:
+ """
+ Search courses by title and description
+ """
+ return await search_courses(
+ request, current_user, org_slug, query, db_session, page, limit
+ )
+
+
@router.put("/{course_uuid}")
async def api_update_course(
request: Request,
diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py
index 2233d9c9..42b9b49d 100644
--- a/apps/api/src/services/courses/courses.py
+++ b/apps/api/src/services/courses/courses.py
@@ -1,6 +1,6 @@
from typing import Literal, List
from uuid import uuid4
-from sqlmodel import Session, select, or_, and_
+from sqlmodel import Session, select, or_, and_, text
from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization
@@ -214,6 +214,80 @@ async def get_courses_orgslug(
return course_reads
+async def search_courses(
+ request: Request,
+ current_user: PublicUser | AnonymousUser,
+ org_slug: str,
+ search_query: str,
+ db_session: Session,
+ page: int = 1,
+ limit: int = 10,
+) -> List[CourseRead]:
+ offset = (page - 1) * limit
+
+ # Base query
+ query = (
+ select(Course)
+ .join(Organization)
+ .where(Organization.slug == org_slug)
+ .where(
+ or_(
+ text(f"LOWER(course.name) LIKE LOWER('%{search_query}%')"),
+ text(f"LOWER(course.description) LIKE LOWER('%{search_query}%')"),
+ text(f"LOWER(course.about) LIKE LOWER('%{search_query}%')"),
+ text(f"LOWER(course.learnings) LIKE LOWER('%{search_query}%')"),
+ text(f"LOWER(course.tags) LIKE LOWER('%{search_query}%')")
+ )
+ )
+ )
+
+ if isinstance(current_user, AnonymousUser):
+ # For anonymous users, only show public courses
+ query = query.where(Course.public == True)
+ else:
+ # For authenticated users, show:
+ # 1. Public courses
+ # 2. Courses not in any UserGroup
+ # 3. Courses in UserGroups where the user is a member
+ # 4. Courses where the user is a resource author
+ query = (
+ query
+ .outerjoin(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore
+ .outerjoin(UserGroupUser, and_(
+ UserGroupUser.usergroup_id == UserGroupResource.usergroup_id,
+ UserGroupUser.user_id == current_user.id
+ ))
+ .outerjoin(ResourceAuthor, ResourceAuthor.resource_uuid == Course.course_uuid) # type: ignore
+ .where(or_(
+ Course.public == True,
+ UserGroupResource.resource_uuid == None, # Courses not in any UserGroup # noqa: E711
+ UserGroupUser.user_id == current_user.id, # Courses in UserGroups where user is a member
+ ResourceAuthor.user_id == current_user.id # Courses where user is a resource author
+ ))
+ )
+
+ # Apply pagination
+ query = query.offset(offset).limit(limit).distinct()
+
+ courses = db_session.exec(query).all()
+
+ # Fetch authors for each course
+ course_reads = []
+ for course in courses:
+ authors_query = (
+ select(User)
+ .join(ResourceAuthor, ResourceAuthor.user_id == User.id) # type: ignore
+ .where(ResourceAuthor.resource_uuid == course.course_uuid)
+ )
+ authors = db_session.exec(authors_query).all()
+
+ course_read = CourseRead.model_validate(course)
+ course_read.authors = [UserRead.model_validate(author) for author in authors]
+ course_reads.append(course_read)
+
+ return course_reads
+
+
async def create_course(
request: Request,
org_id: int,
diff --git a/apps/web/components/Objects/Menus/OrgMenu.tsx b/apps/web/components/Objects/Menus/OrgMenu.tsx
index 22eef5c2..45ef6ad8 100644
--- a/apps/web/components/Objects/Menus/OrgMenu.tsx
+++ b/apps/web/components/Objects/Menus/OrgMenu.tsx
@@ -1,12 +1,14 @@
'use client'
import React from 'react'
import Link from 'next/link'
+import { Search } from 'lucide-react'
import { getUriWithOrg } from '@services/config/config'
import { HeaderProfileBox } from '@components/Security/HeaderProfileBox'
import MenuLinks from './OrgMenuLinks'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
+import { SearchBar } from '@components/Objects/Search/SearchBar'
export const OrgMenu = (props: any) => {
const orgslug = props.orgslug
@@ -50,6 +52,12 @@ export const OrgMenu = (props: any) => {
{course.description}
+ {course.authors && course.authors[0] && ( ++ by {course.authors[0].first_name} {course.authors[0].last_name} +
+ )} +