From 45b7f13d1bf0fb7a026c08a09a11adc5cbc98cdd Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 23 Feb 2026 14:58:05 -0500 Subject: [PATCH] Featured carousel, mobile nav: backdrop below navbar, close on overlay/hamburger Co-authored-by: Cursor --- apps/providers/views.py | 77 ++++++++++++++++ templates/base.html | 55 ++++++++--- templates/providers/category_list.html | 107 +++++++++++++++------- templates/providers/smart_match_quiz.html | 19 +++- 4 files changed, 213 insertions(+), 45 deletions(-) diff --git a/apps/providers/views.py b/apps/providers/views.py index 57619b6..c1939b0 100644 --- a/apps/providers/views.py +++ b/apps/providers/views.py @@ -172,6 +172,69 @@ def _get_rating_distribution(self, provider): return distribution +def _get_category_sections(): + """Group category names into section types for display (e.g. mobile).""" + from collections import defaultdict + section_map = { + 'Plumbing': 'Trades & Home', + 'Electrical': 'Trades & Home', + 'HVAC': 'Trades & Home', + 'Carpentry': 'Trades & Home', + 'Auto Repair': 'Trades & Home', + 'Cleaning': 'Trades & Home', + 'Landscaping': 'Trades & Home', + 'Painting': 'Trades & Home', + 'Roofing': 'Trades & Home', + 'Handyman': 'Trades & Home', + 'Graphic Design': 'Creative & Digital', + 'Web Development': 'Creative & Digital', + 'Photography': 'Creative & Digital', + 'Videography': 'Creative & Digital', + 'Content Writing': 'Creative & Digital', + 'Copywriting': 'Creative & Digital', + 'Social Media Management': 'Creative & Digital', + 'SEO & Marketing': 'Creative & Digital', + 'UI/UX Design': 'Creative & Digital', + 'Animation': 'Creative & Digital', + 'Video Editing': 'Creative & Digital', + 'Illustration': 'Creative & Digital', + 'Accounting': 'Professional Services', + 'Legal Consulting': 'Professional Services', + 'Business Consulting': 'Professional Services', + 'HR Services': 'Professional Services', + 'Career Coaching': 'Professional Services', + 'Financial Planning': 'Professional Services', + 'Tax Preparation': 'Professional Services', + 'Virtual Assistant': 'Professional Services', + 'Translation': 'Professional Services', + 'Personal Training': 'Wellness & Personal', + 'Yoga Instruction': 'Wellness & Personal', + 'Nutrition Coaching': 'Wellness & Personal', + 'Massage Therapy': 'Wellness & Personal', + 'Life Coaching': 'Wellness & Personal', + 'Mental Health Counseling': 'Wellness & Personal', + 'Pet Care': 'Wellness & Personal', + 'Tutoring': 'Wellness & Personal', + 'Music Lessons': 'Wellness & Personal', + 'Event Planning': 'Events & Hospitality', + 'Catering': 'Events & Hospitality', + 'DJ Services': 'Events & Hospitality', + 'Bartending': 'Events & Hospitality', + 'Floral Design': 'Events & Hospitality', + 'Party Entertainment': 'Events & Hospitality', + 'Wedding Planning': 'Events & Hospitality', + 'IT Support': 'Tech & IT', + 'Computer Repair': 'Tech & IT', + 'Network Setup': 'Tech & IT', + 'Cybersecurity': 'Tech & IT', + 'App Development': 'Tech & IT', + 'Database Management': 'Tech & IT', + 'Cloud Services': 'Tech & IT', + 'Tech Training': 'Tech & IT', + } + return section_map + + class CategoryListView(ListView): """List all service categories.""" @@ -183,6 +246,20 @@ def get_queryset(self): return ServiceCategory.objects.filter(is_active=True).annotate( provider_count_annotated=Count('providers', filter=Q(providers__is_active=True)) ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + categories = list(context['categories']) + section_map = _get_category_sections() + sections = {} + for cat in categories: + section_name = section_map.get(cat.name, 'Other') + if section_name not in sections: + sections[section_name] = [] + sections[section_name].append(cat) + order = ['Trades & Home', 'Creative & Digital', 'Professional Services', 'Wellness & Personal', 'Events & Hospitality', 'Tech & IT', 'Other'] + context['category_sections'] = [(name, sections.get(name, [])) for name in order if sections.get(name)] + return context class CategoryDetailView(DetailView): diff --git a/templates/base.html b/templates/base.html index 6a4a882..c2cc2f3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,7 +24,7 @@ @@ -365,7 +378,25 @@

For Pros

+{% endblock %} + diff --git a/templates/providers/smart_match_quiz.html b/templates/providers/smart_match_quiz.html index fdea072..6f20143 100644 --- a/templates/providers/smart_match_quiz.html +++ b/templates/providers/smart_match_quiz.html @@ -431,18 +431,35 @@

// Validate current step function validateStep(step) { const currentStepEl = document.querySelector(`.quiz-step[data-step="${step}"]`); - + // Remove any previous validation message + currentStepEl.querySelectorAll('.quiz-validation-msg').forEach(el => el.remove()); + currentStepEl.classList.remove('quiz-step-invalid'); + if (step === 2) { const city = currentStepEl.querySelector('input[name="city"]'); if (!city.value.trim()) { city.focus(); city.classList.add('border-red-500'); + const msg = document.createElement('p'); + msg.className = 'quiz-validation-msg mt-2 text-red-600 text-sm'; + msg.textContent = 'Please enter your city.'; + city.closest('.max-w-md').appendChild(msg); return false; } city.classList.remove('border-red-500'); } else { const radio = currentStepEl.querySelector('input[type="radio"]:checked'); if (!radio) { + currentStepEl.classList.add('quiz-step-invalid'); + const msg = document.createElement('p'); + msg.className = 'quiz-validation-msg mt-4 text-center text-red-600 font-medium'; + msg.textContent = 'Please select an option to continue.'; + const heading = currentStepEl.querySelector('.text-center.mb-8'); + if (heading && heading.nextElementSibling) { + currentStepEl.insertBefore(msg, heading.nextElementSibling); + } else { + currentStepEl.insertBefore(msg, currentStepEl.firstChild); + } return false; } }