Add autocomplete functionality for parks: implement URLs, views, and templates for real-time suggestions

This commit is contained in:
pacnpal
2025-02-23 13:07:27 -05:00
parent 8e9b6b6a15
commit 1876af46d9
26 changed files with 862 additions and 191 deletions

View File

@@ -1,44 +1,37 @@
/* Alert Styles */
.alert {
@apply fixed z-50 px-4 py-3 transition-all duration-500 transform rounded-lg shadow-lg right-4 top-4;
animation: slideIn 0.5s ease-out forwards;
padding: 1rem;
margin-bottom: 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
opacity: 1;
transition: opacity 0.3s ease-in-out;
cursor: pointer;
}
.alert-success {
@apply text-white bg-green-500;
background-color: #E8F5E9;
border: 1px solid #A5D6A7;
color: #2E7D32;
}
.alert-error {
@apply text-white bg-red-500;
}
.alert-info {
@apply text-white bg-blue-500;
background-color: #FFEBEE;
border: 1px solid #FFCDD2;
color: #C62828;
}
.alert-warning {
@apply text-white bg-yellow-500;
background-color: #FFF3E0;
border: 1px solid #FFCC80;
color: #EF6C00;
}
/* Animation keyframes */
@keyframes slideIn {
0% {
transform: translateX(100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
.alert-info {
background-color: #E3F2FD;
border: 1px solid #90CAF9;
color: #1565C0;
}
@keyframes slideOut {
0% {
transform: translateX(0);
opacity: 1;
}
100% {
transform: translateX(100%);
opacity: 0;
}
.alert.fade-out {
opacity: 0;
}

View File

@@ -2181,6 +2181,18 @@ select {
justify-content: center;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.visible {
visibility: visible;
}
@@ -2457,6 +2469,10 @@ select {
display: none;
}
.h-10 {
height: 2.5rem;
}
.h-16 {
height: 4rem;
}
@@ -2485,6 +2501,10 @@ select {
height: 1.25rem;
}
.h-6 {
height: 1.5rem;
}
.h-8 {
height: 2rem;
}
@@ -2533,6 +2553,10 @@ select {
width: 1.25rem;
}
.w-6 {
width: 1.5rem;
}
.w-64 {
width: 16rem;
}
@@ -2646,6 +2670,16 @@ select {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
@keyframes pulse {
50% {
opacity: .5;
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -3000,10 +3034,6 @@ select {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.bg-gray-900\/80 {
background-color: rgb(17 24 39 / 0.8);
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
@@ -3244,6 +3274,10 @@ select {
padding-bottom: 1rem;
}
.pt-2 {
padding-top: 0.5rem;
}
.text-left {
text-align: left;
}
@@ -3335,6 +3369,11 @@ select {
color: rgb(37 99 235 / var(--tw-text-opacity));
}
.text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity));
@@ -3405,6 +3444,11 @@ select {
color: rgb(79 70 229 / var(--tw-text-opacity));
}
.text-red-100 {
--tw-text-opacity: 1;
color: rgb(254 226 226 / var(--tw-text-opacity));
}
.text-red-400 {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
@@ -3507,6 +3551,11 @@ select {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
.ring-2 {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
@@ -3522,6 +3571,19 @@ select {
--tw-ring-color: rgb(79 70 229 / 0.2);
}
.ring-offset-2 {
--tw-ring-offset-width: 2px;
}
.ring-offset-white {
--tw-ring-offset-color: #fff;
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
@@ -3796,6 +3858,11 @@ select {
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.focus\:bg-gray-100:focus {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.focus\:underline:focus {
text-decoration-line: underline;
}
@@ -3824,6 +3891,10 @@ select {
--tw-ring-offset-width: 2px;
}
.active\:transform:active {
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.disabled\:opacity-50:disabled {
opacity: 0.5;
}
@@ -3930,6 +4001,10 @@ select {
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.dark\:bg-red-900\/40:is(.dark *) {
background-color: rgb(127 29 29 / 0.4);
}
.dark\:bg-yellow-200:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(254 240 138 / var(--tw-bg-opacity));
@@ -3968,6 +4043,11 @@ select {
--tw-gradient-to: #3b0764 var(--tw-gradient-to-position);
}
.dark\:text-blue-100:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(219 234 254 / var(--tw-text-opacity));
}
.dark\:text-blue-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(191 219 254 / var(--tw-text-opacity));
@@ -4190,6 +4270,11 @@ select {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:focus\:bg-gray-700:focus:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
@media (min-width: 640px) {
.sm\:col-span-3 {
grid-column: span 3 / span 3;
@@ -4297,10 +4382,26 @@ select {
grid-column: span 2 / span 2;
}
.md\:col-span-3 {
grid-column: span 3 / span 3;
}
.md\:mb-8 {
margin-bottom: 2rem;
}
.md\:block {
display: block;
}
.md\:grid {
display: grid;
}
.md\:hidden {
display: none;
}
.md\:h-\[140px\] {
height: 140px;
}

View File

@@ -0,0 +1,22 @@
{
const data = document.currentScript.dataset;
const isDebug = data.debug === "True";
if (isDebug) {
document.addEventListener("htmx:beforeOnLoad", function (event) {
const xhr = event.detail.xhr;
if (xhr.status == 500 || xhr.status == 404) {
// Tell htmx to stop processing this response
event.stopPropagation();
document.children[0].innerHTML = xhr.response;
// Run Djangos inline script
// (1, eval) wtf - see https://stackoverflow.com/questions/9107240/1-evalthis-vs-evalthis-in-javascript
(1, eval)(document.scripts[0].innerText);
// Need to directly call Djangos onload function since browser wont
window.onload();
}
});
}
}

21
staticfiles/js/alerts.js Normal file
View File

@@ -0,0 +1,21 @@
document.addEventListener('DOMContentLoaded', () => {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
// Auto-hide alerts after 5 seconds
setTimeout(() => {
alert.classList.add('fade-out');
setTimeout(() => {
alert.remove();
}, 300); // Match CSS transition duration
}, 5000);
// Add click-to-dismiss functionality
alert.addEventListener('click', () => {
alert.classList.add('fade-out');
setTimeout(() => {
alert.remove();
}, 300);
});
});
});

40
staticfiles/js/main.js Normal file
View File

@@ -0,0 +1,40 @@
// Theme Toggle
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = themeToggle.nextElementSibling.querySelector('i');
// Set initial icon
updateThemeIcon();
themeToggle.addEventListener('change', () => {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
updateThemeIcon();
});
function updateThemeIcon() {
const isDark = document.documentElement.classList.contains('dark');
themeIcon.classList.remove('fa-sun', 'fa-moon');
themeIcon.classList.add(isDark ? 'fa-sun' : 'fa-moon');
}
// Mobile Menu Toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
const menuIcon = mobileMenuBtn.querySelector('i');
mobileMenu.style.display = 'none';
let isMenuOpen = false;
mobileMenuBtn.addEventListener('click', () => {
isMenuOpen = !isMenuOpen;
mobileMenu.style.display = isMenuOpen ? 'block' : 'none';
menuIcon.classList.remove('fa-bars', 'fa-times');
menuIcon.classList.add(isMenuOpen ? 'fa-times' : 'fa-bars');
});
});

View File

@@ -0,0 +1,134 @@
/* Loading states */
.htmx-request .htmx-indicator {
opacity: 1;
}
.htmx-request.htmx-indicator {
opacity: 1;
}
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in-out;
}
/* Results container transitions */
#park-results {
transition: opacity 200ms ease-in-out;
}
.htmx-request #park-results {
opacity: 0.7;
}
.htmx-settling #park-results {
opacity: 1;
}
/* Grid/List transitions */
.park-card {
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
background-color: white;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
}
/* Grid view styles */
.park-card[data-view-mode="grid"] {
display: flex;
flex-direction: column;
}
.park-card[data-view-mode="grid"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
/* List view styles */
.park-card[data-view-mode="list"] {
display: flex;
gap: 1rem;
padding: 1rem;
}
.park-card[data-view-mode="list"]:hover {
background-color: #f9fafb;
}
/* Image containers */
.park-card .image-container {
position: relative;
overflow: hidden;
}
.park-card[data-view-mode="grid"] .image-container {
aspect-ratio: 16 / 9;
width: 100%;
}
.park-card[data-view-mode="list"] .image-container {
width: 6rem;
height: 6rem;
flex-shrink: 0;
}
/* Content */
.park-card .content {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0; /* Enables text truncation in flex child */
}
/* Status badges */
.park-card .status-badge {
transition: all 150ms ease-in-out;
}
.park-card:hover .status-badge {
transform: scale(1.05);
}
/* Images */
.park-card img {
transition: transform 200ms ease-in-out;
object-fit: cover;
width: 100%;
height: 100%;
}
.park-card:hover img {
transform: scale(1.05);
}
/* Placeholders for missing images */
.park-card .placeholder {
background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%);
background-size: 200% 100%;
animation: shimmer 1.5s linear infinite;
}
@keyframes shimmer {
to {
background-position: 200% center;
}
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.park-card {
background-color: #1f2937;
border-color: #374151;
}
.park-card[data-view-mode="list"]:hover {
background-color: #374151;
}
.park-card .text-gray-900 {
color: #f3f4f6;
}
.park-card .text-gray-500 {
color: #9ca3af;
}
.park-card .placeholder {
background: linear-gradient(110deg, #2d3748 8%, #374151 18%, #2d3748 33%);
}
.park-card[data-view-mode="list"]:hover {
background-color: #374151;
}
}

View File

@@ -0,0 +1,69 @@
// Handle view mode persistence across HTMX requests
document.addEventListener('htmx:configRequest', function(evt) {
// Preserve view mode
const parkResults = document.getElementById('park-results');
if (parkResults) {
const viewMode = parkResults.getAttribute('data-view-mode');
if (viewMode) {
evt.detail.parameters['view_mode'] = viewMode;
}
}
// Preserve search terms
const searchInput = document.getElementById('search');
if (searchInput && searchInput.value) {
evt.detail.parameters['search'] = searchInput.value;
}
});
// Handle loading states
document.addEventListener('htmx:beforeRequest', function(evt) {
const target = evt.detail.target;
if (target) {
target.classList.add('htmx-requesting');
}
});
document.addEventListener('htmx:afterRequest', function(evt) {
const target = evt.detail.target;
if (target) {
target.classList.remove('htmx-requesting');
}
});
// Handle history navigation
document.addEventListener('htmx:historyRestore', function(evt) {
const parkResults = document.getElementById('park-results');
if (parkResults && evt.detail.path) {
const url = new URL(evt.detail.path, window.location.origin);
const viewMode = url.searchParams.get('view_mode');
if (viewMode) {
parkResults.setAttribute('data-view-mode', viewMode);
}
}
});
// Initialize lazy loading for images
function initializeLazyLoading(container) {
if (!('IntersectionObserver' in window)) return;
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
imageObserver.unobserve(img);
}
});
});
container.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
}
// Initialize lazy loading after HTMX content swaps
document.addEventListener('htmx:afterSwap', function(evt) {
initializeLazyLoading(evt.detail.target);
});