diff --git a/web/migrations/0064_alter_goods_options_alter_order_options.py b/web/migrations/0064_alter_goods_options_alter_order_options.py new file mode 100644 index 000000000..e9d7cd91a --- /dev/null +++ b/web/migrations/0064_alter_goods_options_alter_order_options.py @@ -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']}, + ), + ] diff --git a/web/models.py b/web/models.py index 6b8ea8ef4..3a26948e3 100644 --- a/web/models.py +++ b/web/models.py @@ -1318,6 +1318,9 @@ def save(self, *args, **kwargs): self.slug = slug super().save(*args, **kwargs) + class Meta: + ordering = ["-created_at"] + def __str__(self): return f"{self.name} (${self.price})" @@ -1636,6 +1639,9 @@ def save(self, *args, **kwargs): def generate_tracking_number(self): return f"TRACK-{self.pk}-{int(time.time())}-{uuid.uuid4().hex[:6].upper()}" + class Meta: + ordering = ["-created_at"] + def __str__(self): return f"Order #{self.id} ({self.user.email})" diff --git a/web/templates/base.html b/web/templates/base.html index 7afa700a9..cac546582 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -343,16 +343,22 @@ - {% if request.user.cart.item_count > 0 or request.session.session_key and request.session.session_key|get_cart_item_count > 0 %} - - {% 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 %} + + {% if request.user.is_authenticated %} + {{ item_count }} + {% else %} + {{ s_count }} + {% endif %} + {% endif %} - - {% endif %} + {% endwith %} + {% endwith %}
-
+ + 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" />
+
@@ -758,15 +774,19 @@ Cart
- {% if request.user.cart.item_count > 0 or request.session.session_key and request.session.session_key|get_cart_item_count > 0 %} - - {% 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 %} + + {% if request.user.is_authenticated %} + {{ item_count }} + {% else %} + {{ s_count }} + {% endif %} + {% endif %} - - {% endif %} + {% endwith %} + {% endwith %}
{% if user.is_authenticated %} @@ -1060,7 +1080,8 @@

CONNECT WITH About Us Terms & Conditions + class="hover:text-teal-600 dark:hover:text-teal-400">Terms & + Conditions Privacy Policy CONNECT WITH } }); } + + // Global Search Functionality + document.addEventListener('DOMContentLoaded', function() { + 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); + 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 => ` + +
+ +
+
+
${result.title}
+
${result.type}
+
+
+ `).join(''); + resultsContainer.classList.remove('hidden'); + } else { + resultsContainer.innerHTML = ` +
+ No results found for "${query}" +
+ `; + resultsContainer.classList.remove('hidden'); + } + }); + }, 300); + }); + + // Close results on escape + input.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + resultsContainer.classList.add('hidden'); + } + }); + }); + + // 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'); + }); + } + }); + }); {% block extra_js %} {% endblock extra_js %} diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..8e511d00f 100644 --- a/web/urls.py +++ b/web/urls.py @@ -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"), path("courses//", views.course_detail, name="course_detail"), path("courses//enroll/", views.enroll_course, name="enroll_course"), path("courses//add-session/", views.add_session, name="add_session"), diff --git a/web/views.py b/web/views.py index b3a310687..25fc4433b 100644 --- a/web/views.py +++ b/web/views.py @@ -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": []}) + + 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: + results.append( + { + "type": "course", + "title": course.title, + "url": reverse("course_detail", kwargs={"slug": course.slug}), + "icon": "fas fa-book", + } + ) + + return JsonResponse({"results": results})