diff --git a/backend/static/js/alpine-components.js b/backend/static/js/alpine-components.js index f2776d26..04ab9803 100644 --- a/backend/static/js/alpine-components.js +++ b/backend/static/js/alpine-components.js @@ -78,10 +78,30 @@ Alpine.data('searchComponent', () => ({ this.loading = true; try { - const response = await fetch(`/api/search/?q=${encodeURIComponent(this.query)}`); - const data = await response.json(); - this.results = data.results || []; - this.showResults = this.results.length > 0; + // Use the same search endpoint as HTMX in the template + const response = await fetch(`/search/parks/?q=${encodeURIComponent(this.query)}`, { + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + } + }); + + if (response.ok) { + // Try to parse as JSON first, fallback to extracting from HTML + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + this.results = data.results || []; + } else { + // Parse HTML response to extract search results + const html = await response.text(); + this.results = this.parseSearchResults(html); + } + this.showResults = this.results.length > 0; + } else { + this.results = []; + this.showResults = false; + } } catch (error) { console.error('Search error:', error); this.results = []; @@ -91,6 +111,36 @@ Alpine.data('searchComponent', () => ({ } }, + parseSearchResults(html) { + // Helper method to extract search results from HTML response + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const results = []; + + // Look for search result items in the HTML + const resultElements = doc.querySelectorAll('[data-search-result], .search-result-item, .park-item'); + resultElements.forEach(element => { + const title = element.querySelector('h3, .title, [data-title]')?.textContent?.trim(); + const url = element.querySelector('a')?.getAttribute('href'); + const description = element.querySelector('.description, .excerpt, p')?.textContent?.trim(); + + if (title && url) { + results.push({ + title, + url, + description: description || '' + }); + } + }); + + return results.slice(0, 10); // Limit to 10 results + } catch (error) { + console.error('Error parsing search results:', error); + return []; + } + }, + selectResult(result) { window.location.href = result.url; this.showResults = false; @@ -442,16 +492,34 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ }, body: new URLSearchParams({ login: this.loginForm.username, - password: this.loginForm.password - }) + password: this.loginForm.password, + next: window.location.pathname + }), + redirect: 'manual' // Handle redirects manually }); - if (response.ok) { - // Login successful - reload page to update auth state - window.location.reload(); + // Django allauth returns 302 redirect on successful login + if (response.status === 302 || (response.ok && response.status === 200)) { + // Check if login was successful by trying to parse response + try { + const html = await response.text(); + // If response contains error messages, login failed + if (html.includes('errorlist') || html.includes('alert-danger') || html.includes('invalid')) { + this.loginError = this.extractErrorFromHTML(html) || 'Login failed. Please check your credentials.'; + } else { + // Login successful - reload page to update auth state + window.location.reload(); + } + } catch { + // If we can't parse response, assume success and reload + window.location.reload(); + } + } else if (response.status === 200) { + // Form validation errors - parse HTML response for error messages + const html = await response.text(); + this.loginError = this.extractErrorFromHTML(html) || 'Login failed. Please check your credentials.'; } else { - const data = await response.json(); - this.loginError = data.message || 'Login failed. Please check your credentials.'; + this.loginError = 'Login failed. Please check your credentials.'; } } catch (error) { console.error('Login error:', error); @@ -492,17 +560,32 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ username: this.registerForm.username, password1: this.registerForm.password1, password2: this.registerForm.password2 - }) + }), + redirect: 'manual' }); - if (response.ok) { - // Registration successful - this.close(); - // Show success message or redirect - Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); + if (response.status === 302 || response.ok) { + try { + const html = await response.text(); + // Check if registration was successful + if (html.includes('errorlist') || html.includes('alert-danger') || html.includes('invalid')) { + this.registerError = this.extractErrorFromHTML(html) || 'Registration failed. Please try again.'; + } else { + // Registration successful + this.close(); + Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); + } + } catch { + // Assume success if we can't parse response + this.close(); + Alpine.store('toast').success('Account created successfully! Please check your email to verify your account.'); + } + } else if (response.status === 200) { + // Form validation errors + const html = await response.text(); + this.registerError = this.extractErrorFromHTML(html) || 'Registration failed. Please try again.'; } else { - const data = await response.json(); - this.registerError = data.message || 'Registration failed. Please try again.'; + this.registerError = 'Registration failed. Please try again.'; } } catch (error) { console.error('Registration error:', error); @@ -523,6 +606,38 @@ Alpine.data('authModal', (defaultMode = 'login') => ({ window.location.href = provider.auth_url; }, + extractErrorFromHTML(html) { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // Look for error messages in various formats + const errorSelectors = [ + '.errorlist li', + '.alert-danger', + '.invalid-feedback', + '.form-error', + '[class*="error"]', + '.field-error' + ]; + + for (const selector of errorSelectors) { + const errorElements = doc.querySelectorAll(selector); + if (errorElements.length > 0) { + return Array.from(errorElements) + .map(el => el.textContent.trim()) + .filter(text => text.length > 0) + .join(' '); + } + } + + return null; + } catch (error) { + console.error('Error parsing HTML for error messages:', error); + return null; + } + }, + getCSRFToken() { const token = document.querySelector('[name=csrfmiddlewaretoken]')?.value || document.querySelector('meta[name=csrf-token]')?.getAttribute('content') || diff --git a/backend/static/js/main.js b/backend/static/js/main.js index 9495dba0..088434a3 100644 --- a/backend/static/js/main.js +++ b/backend/static/js/main.js @@ -51,8 +51,10 @@ document.addEventListener('DOMContentLoaded', () => { // Update icon const icon = mobileMenuBtn.querySelector('i'); - icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times'); - icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars'); + if (icon) { + icon.classList.remove(isMenuOpen ? 'fa-bars' : 'fa-times'); + icon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars'); + } }; mobileMenuBtn.addEventListener('click', toggleMenu);