Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions web/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@
<div class="relative hidden lg:inline-block w-[250px]">
<form action="{% url 'course_search' %}" method="get" class="m-0">
<input type="text"
id="search-input-desktop"
autocomplete="off"
name="q"
placeholder="What do you want to learn?"
class="rounded-full w-[250px] bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-teal-300 dark:focus:ring-teal-700" />
Expand All @@ -353,14 +355,13 @@
<i class="fas fa-search"></i>
</button>
</form>
<div id="autocomplete-dropdown" role="listbox" aria-label="Course suggestions" class="absolute z-50 w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-1 hidden border border-gray-200 dark:border-gray-700"></div>
</div>
<!-- Messaging Button -->
<a href="{% url 'inbox' %}"
title="Messages"
class="flex items-center p-2 hover:bg-teal-700 rounded-lg">
<i class="fas fa-comments text-xl"></i>
</a> <!-- Language and Dark Mode -->
<div class="flex items-center space-x-2">
<!-- Cart Icon -->
<a href="{% url 'cart_view' %}"
class="relative hover:underline flex items-center p-2 hover:bg-teal-700 rounded-lg">
Expand Down Expand Up @@ -1119,5 +1120,71 @@ <h3 class="text-lg font-bold mb-4 text-gray-700 dark:text-gray-200">CONNECT WITH
</script>
{% block extra_js %}
{% endblock extra_js %}
<script>
(function() {
const input = document.getElementById('search-input-desktop');
const dropdown = document.getElementById('autocomplete-dropdown');
if (!input || !dropdown) return;

let debounceTimer;
let currentController = null;

input.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
dropdown.classList.add('hidden');
dropdown.innerHTML = '';
return;
}
debounceTimer = setTimeout(() => {
if (currentController) currentController.abort();
currentController = new AbortController();
fetch("{% url 'search_autocomplete' %}" + '?q=' + encodeURIComponent(query), { signal: currentController.signal })
.then(r => r.json())
.then(data => {
if (!data.results || data.results.length === 0) {
dropdown.classList.add('hidden');
dropdown.innerHTML = '';
return;
}
dropdown.replaceChildren();
data.results.forEach((item) => {
const a = document.createElement('a');
a.href = item.url;
a.setAttribute('role', 'option');
a.setAttribute('aria-selected', 'false');
a.className = 'flex items-center px-4 py-2 hover:bg-teal-50 dark:hover:bg-gray-700 text-sm text-gray-800 dark:text-gray-200 border-b border-gray-100 dark:border-gray-700 last:border-0';
const icon = document.createElement('i');
icon.className = 'fas fa-book-open mr-2 text-teal-500 text-xs';
const title = document.createElement('span');
title.className = 'font-medium';
title.textContent = item.title;
const teacher = document.createElement('span');
teacher.className = 'ml-auto text-xs text-gray-400';
teacher.textContent = item.teacher;
a.append(icon, title, teacher);
dropdown.appendChild(a);
});
dropdown.classList.remove('hidden');
})
.catch(() => {
dropdown.classList.add('hidden');
dropdown.replaceChildren();
});
}, 250);
});

document.addEventListener('click', function(e) {
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.classList.add('hidden');
}
});

input.addEventListener('keydown', function(e) {
if (e.key === 'Escape') dropdown.classList.add('hidden');
});
})();
</script>
</body>
</html>
1 change: 1 addition & 0 deletions web/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
# Course Management
path("courses/create/", views.create_course, name="create_course"),
path("courses/search/", views.course_search, name="course_search"),
path("search/autocomplete/", views.search_autocomplete, name="search_autocomplete"),
path("courses/<slug:slug>/", views.course_detail, name="course_detail"),
path("courses/<slug:course_slug>/enroll/", views.enroll_course, name="enroll_course"),
path("courses/<slug:slug>/add-session/", views.add_session, name="add_session"),
Expand Down
42 changes: 42 additions & 0 deletions web/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,48 @@ def send_welcome_teach_course_email(request, user, temp_password):
)



@require_GET
def search_autocomplete(request: HttpRequest) -> JsonResponse:
"""Return course autocomplete results for navbar search.

Args:
request: HTTP GET request with 'q' query parameter.

Returns:
JsonResponse with list of matching courses (up to 8).
"""
query = request.GET.get("q", "").strip()
if len(query) > 64:
return JsonResponse({"results": []})
results = []
if len(query) >= 2:
courses = Course.objects.filter(
status="published"
).filter(
Q(title__icontains=query)
| Q(tags__icontains=query)
| Q(teacher__username__icontains=query)
| Q(teacher__first_name__icontains=query)
| Q(teacher__last_name__icontains=query)
).order_by("title").values(
"title", "slug", "teacher__username", "teacher__first_name", "teacher__last_name"
)[:8]
for course in courses:
teacher_name = " ".join(
part for part in [course["teacher__first_name"], course["teacher__last_name"]] if part
) or course["teacher__username"]
results.append(
{
"type": "course",
"title": course["title"],
"url": reverse("course_detail", kwargs={"slug": course["slug"]}),
"teacher": teacher_name,
}
)
return JsonResponse({"results": results})


def course_search(request):
query = request.GET.get("q", "")
subject = request.GET.get("subject", "")
Expand Down