-
-
Notifications
You must be signed in to change notification settings - Fork 163
feat: Add Global Navbar Search #938
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
base: main
Are you sure you want to change the base?
Changes from all commits
6cd33d2
796b160
75f2539
216ea3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Generated by Django 5.1.15 on 2026-02-23 05:38 | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('web', '0063_virtualclassroom_virtualclassroomcustomization_and_more'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterModelOptions( | ||
| name='goods', | ||
| options={'ordering': ['-created_at']}, | ||
| ), | ||
| migrations.AlterModelOptions( | ||
| name='order', | ||
| options={'ordering': ['-created_at']}, | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -343,16 +343,22 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Search bar (persistent element) --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="relative hidden lg:inline-block w-[250px]"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form action="{% url 'course_search' %}" method="get" class="m-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form action="{% url 'course_search' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method="get" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="m-0" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="desktop-search-form"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="q" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| autocomplete="off" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="navbar-search-input 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" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Replace custom CSS classes with
♻️ Proposed fixIn the desktop search input (line 354): -class="navbar-search-input rounded-full w-[250px] ..."
+data-search-input class="rounded-full w-[250px] ..."In the desktop results container (line 360): -<div class="navbar-search-results hidden absolute top-full ...">
+<div data-search-results class="hidden absolute top-full ...">Apply the same changes at lines 563 and 569 for the mobile equivalents. In the JavaScript (lines 1155, 1159, 1211): -const searchInputs = document.querySelectorAll('.navbar-search-input');
+const searchInputs = document.querySelectorAll('[data-search-input]');
...
-const resultsContainer = input.closest('.relative').querySelector('.navbar-search-results');
+const resultsContainer = input.closest('.relative').querySelector('[data-search-results]');
...
-document.querySelectorAll('.navbar-search-results').forEach(...)
+document.querySelectorAll('[data-search-results]').forEach(...)As per coding guidelines: "Never use custom CSS classes" ( Also applies to: 360-360, 563-563, 569-569 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button type="submit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="absolute right-3 top-2 text-gray-500 dark:text-gray-300"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <i class="fas fa-search"></i> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="navbar-search-results hidden absolute top-full left-0 right-0 mt-2 bg-white dark:bg-gray-800 rounded-lg shadow-xl z-[100] max-h-96 overflow-y-auto border border-gray-100 dark:border-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Messaging Button --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'inbox' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -365,15 +371,19 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'cart_view' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="relative hover:underline flex items-center p-2 hover:bg-teal-700 rounded-lg"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <i class="fa-solid fa-shopping-cart"></i> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.cart.item_count > 0 or request.session.session_key and request.session.session_key|get_cart_item_count > 0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.is_authenticated %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ request.user.cart.item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% else %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ request.session.session_key|get_cart_item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% with item_count=request.user.cart.item_count|default:0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% with s_count=request.session.session_key|get_cart_item_count|default:0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if item_count > 0 or s_count > 0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.is_authenticated %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% else %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ s_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endwith %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endwith %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+374
to
+386
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cart badge can render If 🐛 Proposed fix (desktop; apply the same to the mobile block at lines 777–789)-{% with item_count=request.user.cart.item_count|default:0 %}
- {% with s_count=request.session.session_key|get_cart_item_count|default:0 %}
- {% if item_count > 0 or s_count > 0 %}
- <span class="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
- {% if request.user.is_authenticated %}
- {{ item_count }}
- {% else %}
- {{ s_count }}
- {% endif %}
- </span>
- {% endif %}
- {% endwith %}
-{% endwith %}
+{% if request.user.is_authenticated %}
+ {% with item_count=request.user.cart.item_count|default:0 %}
+ {% if item_count > 0 %}
+ <span class="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
+ {{ item_count }}
+ </span>
+ {% endif %}
+ {% endwith %}
+{% else %}
+ {% with s_count=request.session.session_key|get_cart_item_count|default:0 %}
+ {% if s_count > 0 %}
+ <span class="absolute -top-1 -right-1 bg-orange-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
+ {{ s_count }}
+ </span>
+ {% endif %}
+ {% endwith %}
+{% endif %}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- New Notification Button for Invitations --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'user_invitations' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -542,16 +552,22 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="pt-16 px-4 pb-8"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Search bar --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="relative mb-6"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form action="{% url 'course_search' %}" method="get" class="m-0"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form action="{% url 'course_search' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method="get" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="m-0" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="mobile-search-form"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name="q" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| autocomplete="off" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="What do you want to learn?" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="w-full rounded-full bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-300 dark:focus:ring-teal-700" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="navbar-search-input w-full rounded-full bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-300 dark:focus:ring-teal-700" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button type="submit" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="absolute right-3 top-2.5 text-gray-500 dark:text-gray-400"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <i class="fas fa-search"></i> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="navbar-search-results hidden absolute top-full left-0 right-0 mt-2 bg-white dark:bg-gray-800 rounded-lg shadow-xl z-[100] max-h-96 overflow-y-auto border border-gray-100 dark:border-gray-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Mobile Navigation Menu with Accordions --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -758,15 +774,19 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <i class="fa-solid fa-shopping-cart mr-2 text-teal-500"></i> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>Cart</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.cart.item_count > 0 or request.session.session_key and request.session.session_key|get_cart_item_count > 0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="bg-orange-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.is_authenticated %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ request.user.cart.item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% else %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ request.session.session_key|get_cart_item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% with item_count=request.user.cart.item_count|default:0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% with s_count=request.session.session_key|get_cart_item_count|default:0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if item_count > 0 or s_count > 0 %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span class="bg-orange-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if request.user.is_authenticated %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ item_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% else %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {{ s_count }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endif %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endwith %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endwith %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- User Account Section --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% if user.is_authenticated %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1060,7 +1080,8 @@ <h3 class="text-lg font-bold mb-4 text-gray-700 dark:text-gray-200">CONNECT WITH | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'about' %}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="hover:text-teal-600 dark:hover:text-teal-400">About Us</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'terms' %}#terms" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="hover:text-teal-600 dark:hover:text-teal-400">Terms & Conditions</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="hover:text-teal-600 dark:hover:text-teal-400">Terms & | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Conditions</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'terms' %}#privacy" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="hover:text-teal-600 dark:hover:text-teal-400">Privacy Policy</a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="{% url 'terms' %}#cookies" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -1116,6 +1137,71 @@ <h3 class="text-lg font-bold mb-4 text-gray-700 dark:text-gray-200">CONNECT WITH | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Global Search Functionality | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener('DOMContentLoaded', function() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const searchInputs = document.querySelectorAll('.navbar-search-input'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let debounceTimer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| searchInputs.forEach(input => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1143
to
+1146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Shared The single ♻️ Proposed fix- const searchInputs = document.querySelectorAll('.navbar-search-input');
- let debounceTimer;
-
- searchInputs.forEach(input => {
- const resultsContainer = input.closest('.relative').querySelector('.navbar-search-results');
-
- input.addEventListener('input', function() {
- clearTimeout(debounceTimer);
+ document.querySelectorAll('[data-search-input]').forEach(input => {
+ let debounceTimer;
+ const resultsContainer = input.closest('.relative').querySelector('[data-search-results]');
+
+ input.addEventListener('input', function() {
+ clearTimeout(debounceTimer);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resultsContainer = input.closest('.relative').querySelector('.navbar-search-results'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input.addEventListener('input', function() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| clearTimeout(debounceTimer); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const query = this.value.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (query.length < 2) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.innerHTML = ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.classList.add('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| debounceTimer = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetch(`/api/search/?q=${encodeURIComponent(query)}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(response => response.json()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(data => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.results.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.innerHTML = data.results.map(result => ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href="${result.url}" class="flex items-center px-4 py-3 hover:bg-teal-50 dark:hover:bg-teal-900/30 border-b border-gray-100 dark:border-gray-700 last:border-0 transition-colors"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full bg-teal-100 dark:bg-teal-900 text-teal-600 dark:text-teal-400 mr-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <i class="${result.icon}"></i> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="text-sm font-semibold text-gray-900 dark:text-gray-100">${result.title}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="text-xs text-gray-500 dark:text-gray-400 capitalize">${result.type}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `).join(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.classList.remove('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.innerHTML = ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| No results found for "${query}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1164
to
+1181
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical XSS — never inject unsanitized data into Four vectors exist in this block:
Use DOM construction with 🔒 Proposed safe DOM builder-resultsContainer.innerHTML = data.results.map(result => `
- <a href="${result.url}" class="flex items-center px-4 py-3 ...">
- <div class="...">
- <i class="${result.icon}"></i>
- </div>
- <div>
- <div class="...">${result.title}</div>
- <div class="...">${result.type}</div>
- </div>
- </a>
-`).join('');
+resultsContainer.innerHTML = '';
+data.results.forEach(result => {
+ const safeUrl = (result.url.startsWith('/') || result.url.startsWith('http')) ? result.url : '#';
+ const a = document.createElement('a');
+ a.href = safeUrl;
+ a.className = 'flex items-center px-4 py-3 hover:bg-teal-50 dark:hover:bg-teal-900/30 border-b border-gray-100 dark:border-gray-700 last:border-0 transition-colors';
+ const iconWrapper = document.createElement('div');
+ iconWrapper.className = 'flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-full bg-teal-100 dark:bg-teal-900 text-teal-600 dark:text-teal-400 mr-3';
+ const icon = document.createElement('i');
+ icon.className = String(result.icon); // class only, no HTML injection
+ iconWrapper.appendChild(icon);
+ const textWrapper = document.createElement('div');
+ const titleDiv = document.createElement('div');
+ titleDiv.className = 'text-sm font-semibold text-gray-900 dark:text-gray-100';
+ titleDiv.textContent = result.title; // safe: textContent escapes HTML
+ const typeDiv = document.createElement('div');
+ typeDiv.className = 'text-xs text-gray-500 dark:text-gray-400 capitalize';
+ typeDiv.textContent = result.type; // safe
+ textWrapper.appendChild(titleDiv);
+ textWrapper.appendChild(typeDiv);
+ a.appendChild(iconWrapper);
+ a.appendChild(textWrapper);
+ resultsContainer.appendChild(a);
+});For the "no results" message: -resultsContainer.innerHTML = `
- <div class="...">No results found for "${query}"</div>
-`;
+const msg = document.createElement('div');
+msg.className = 'px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center';
+msg.textContent = `No results found for "${query}"`;
+resultsContainer.innerHTML = '';
+resultsContainer.appendChild(msg);🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.classList.remove('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1160
to
+1184
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled fetch failures will silently break the search UI. There is no 🛡️ Proposed fix fetch(`/api/search/?q=${encodeURIComponent(query)}`)
.then(response => {
+ if (!response.ok) throw new Error(`Search request failed: ${response.status}`);
return response.json();
})
.then(data => {
- if (data.results.length > 0) {
+ const results = Array.isArray(data.results) ? data.results : [];
+ if (results.length > 0) {
// … render results …
} else {
// … no results …
}
})
+ .catch(() => {
+ resultsContainer.innerHTML = '';
+ resultsContainer.classList.add('hidden');
+ });🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 300); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Close results on escape | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input.addEventListener('keydown', function(e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e.key === 'Escape') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resultsContainer.classList.add('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1146
to
+1194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Search combobox is missing required ARIA attributes for screen readers. The live-results pattern requires Minimum additions to the input elements (lines 350–354 and 559–563): <input type="text"
name="q"
autocomplete="off"
placeholder="What do you want to learn?"
+ role="combobox"
+ aria-autocomplete="list"
+ aria-expanded="false"
+ aria-controls="desktop-search-results"
class="..." />Results container (line 360): -<div class="navbar-search-results hidden absolute ...">
+<div id="desktop-search-results" role="listbox" class="hidden absolute ...">Also toggle +input.setAttribute('aria-expanded', 'true');
resultsContainer.classList.remove('hidden');+input.setAttribute('aria-expanded', 'false');
resultsContainer.classList.add('hidden');As per coding guidelines: "Include proper ARIA labels where needed for accessibility" ( 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Close results when clicking outside | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.addEventListener('click', function(event) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!event.target.closest('.relative')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.querySelectorAll('.navbar-search-results').forEach(container => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| container.classList.add('hidden'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1197
to
+1203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Click-outside guard is too broad —
🛡️ Proposed fix-document.addEventListener('click', function(event) {
- if (!event.target.closest('.relative')) {
- document.querySelectorAll('.navbar-search-results').forEach(container => {
- container.classList.add('hidden');
- });
- }
-});
+document.addEventListener('click', function(event) {
+ // Only keep results open when the click is inside a search container
+ const isInsideSearch = event.target.closest('[data-search-input]') ||
+ event.target.closest('[data-search-results]');
+ if (!isInsideSearch) {
+ document.querySelectorAll('[data-search-results]').forEach(container => {
+ container.classList.add('hidden');
+ });
+ }
+});(This also aligns with the 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% block extra_js %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {% endblock extra_js %} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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("api/search/", views.global_search_api, name="global_search_api"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's verify the urls.py structure and confirm the i18n_patterns context
cat -n web/urls.py | head -120 | tail -40Repository: alphaonelabs/alphaonelabs-education-website Length of output: 3229 🏁 Script executed: # Search for how the search API is being called in the frontend
rg -n "api/search" --type html --type js -C 3Repository: alphaonelabs/alphaonelabs-education-website Length of output: 538 🏁 Script executed: # Check for Django URL template tag usage in base.html
rg -n "url.*global_search" --type htmlRepository: alphaonelabs/alphaonelabs-education-website Length of output: 69 🏁 Script executed: # Examine the i18n_patterns configuration mentioned in the review
rg -n "prefix_default_language|i18n_patterns" web/urls.py -C 2Repository: alphaonelabs/alphaonelabs-education-website Length of output: 576 🏁 Script executed: # Check if there are other API endpoints to see how they handle i18n URLs
rg -n "path.*api/" web/urls.py -A 1Repository: alphaonelabs/alphaonelabs-education-website Length of output: 346 🏁 Script executed: # Look at the broader context of how JavaScript calls other Django URLs in the templates
rg -n "fetch.*url|ajax.*url|\{% url" web/templates/base.html -C 2 | head -50Repository: alphaonelabs/alphaonelabs-education-website Length of output: 4492 🏁 Script executed: # Check if map-data API is also hardcoded or properly handled with template tags
rg -n "api/map-data|map_data" web/templates/base.html -C 2Repository: alphaonelabs/alphaonelabs-education-website Length of output: 69 🏁 Script executed: # Verify the full context of the search implementation to understand severity
cat -n web/templates/base.html | sed -n '1110,1135p'Repository: alphaonelabs/alphaonelabs-education-website Length of output: 1895 Use Django template tag to resolve the search API URL in JavaScript to support i18n URL prefixes. The endpoint is registered inside Resolve the URL dynamically using Django's template tag. For example, wrap the search input in a data attribute: Suggested approach<input type="text" data-search-url="{% url 'global_search_api' %}" ...>Then reference it in JavaScript: const searchUrl = input.getAttribute('data-search-url');
fetch(`${searchUrl}?q=${encodeURIComponent(query)}`)This matches the existing pattern used throughout the template for all other route references (e.g., 🤖 Prompt for AI Agents |
||
| 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"), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8839,3 +8839,38 @@ def leave_session_waiting_room(request, course_slug): | |
| messages.info(request, "You are not in the session waiting room for this course.") | ||
|
|
||
| return redirect("course_detail", slug=course_slug) | ||
|
|
||
|
|
||
| def global_search_api(request): | ||
| """API endpoint for global search of subjects and courses.""" | ||
| query = request.GET.get("q", "").strip() | ||
| if not query or len(query) < 2: | ||
| return JsonResponse({"results": []}) | ||
|
|
||
|
Comment on lines
+8844
to
+8849
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's find and examine web/views.py
fd "views.py" --path "*/web/*"Repository: alphaonelabs/alphaonelabs-education-website Length of output: 302 🏁 Script executed: # Get the file size to understand how to read it efficiently
wc -l web/views.pyRepository: alphaonelabs/alphaonelabs-education-website Length of output: 104 🏁 Script executed: # Read the specific section mentioned in the review (lines 8849-8854 and surrounding context)
# Let's read a broader range to see the full function implementation
sed -n '8845,8885p' web/views.py | cat -nRepository: alphaonelabs/alphaonelabs-education-website Length of output: 1477 🏁 Script executed: # Check if there are any lines exceeding 120 characters in the function
sed -n '8849,8881p' web/views.py | awk 'length > 120 {print NR+8848 ": " length " chars: " $0}'Repository: alphaonelabs/alphaonelabs-education-website Length of output: 69 🏁 Script executed: # Check imports at the top of web/views.py to see what's available
head -50 web/views.py | cat -nRepository: alphaonelabs/alphaonelabs-education-website Length of output: 2025 🏁 Script executed: # Search for `@require_GET` usage in the file to see the pattern
rg "@require_GET" web/views.py -B 2 -A 2Repository: alphaonelabs/alphaonelabs-education-website Length of output: 380 Add a GET-only decorator and type hints for the new API. Line 8849 should include type hints and the Suggested change+@require_GET
-def global_search_api(request):
+def global_search_api(request: HttpRequest) -> JsonResponse:
"""API endpoint for global search of subjects and courses."""🤖 Prompt for AI Agents |
||
| results = [] | ||
|
|
||
| # Search Subjects | ||
| subjects = Subject.objects.filter(name__icontains=query)[:5] | ||
| for subject in subjects: | ||
| results.append( | ||
| { | ||
| "type": "subject", | ||
| "title": subject.name, | ||
| "url": reverse("course_search") + f"?subject={subject.slug}", | ||
| "icon": "fas fa-tag", | ||
| } | ||
| ) | ||
|
|
||
| # Search Courses | ||
| courses = Course.objects.filter(Q(title__icontains=query) | Q(tags__icontains=query), status="published")[:5] | ||
| for course in courses: | ||
|
Comment on lines
+8864
to
+8866
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrap the course filter to stay within the 120‑char limit. ✅ Suggested change- courses = Course.objects.filter(Q(title__icontains=query) | Q(tags__icontains=query), status="published")[:5]
+ courses = Course.objects.filter(
+ Q(title__icontains=query) | Q(tags__icontains=query),
+ status="published",
+ )[:5]As per coding guidelines: Maximum Python line length is 120 characters. 🤖 Prompt for AI Agents |
||
| results.append( | ||
| { | ||
| "type": "course", | ||
| "title": course.title, | ||
| "url": reverse("course_detail", kwargs={"slug": course.slug}), | ||
| "icon": "fas fa-book", | ||
| } | ||
| ) | ||
|
|
||
| return JsonResponse({"results": results}) | ||
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.
🧹 Nitpick | 🔵 Trivial
Suppress RUF012 for migration files via Ruff config rather than editing auto-generated code.
Both
dependencies(lines 8–10) andoperations(lines 12–21) are flagged by Ruff's RUF012 rule ("Mutable class attribute should be annotated withtyping.ClassVar"). This is a well-known false positive for Django migration files — Django's framework intentionally reads these as mutable class-level lists, and annotating them withClassVarin every generated file is impractical.The idiomatic fix is a one-time Ruff config change:
⚙️ Suppress RUF012 for all migration files via
per-file-ignoresIn
pyproject.toml(orruff.toml):[tool.ruff.lint.per-file-ignores] +"*/migrations/*.py" = ["RUF012"]🧰 Tools
🪛 Ruff (0.15.1)
[warning] 8-10: Mutable default value for class attribute
(RUF012)
[warning] 12-21: Mutable default value for class attribute
(RUF012)
🤖 Prompt for AI Agents