Add search

This commit is contained in:
2026-01-18 10:57:54 +11:00
parent 398f8fef20
commit 2d1f8bbd1d
3 changed files with 180 additions and 63 deletions
BIN
View File
Binary file not shown.
+180 -56
View File
@@ -98,22 +98,23 @@
<div class="main-content-grid">
<!-- Left Column - Main Content -->
<div class="left-column">
<!-- Popular Links Section -->
<div>
<h2 class="section-title">{% trans "Popular Links" %}</h2>
<div class="popular-links-desktop">
{% for item in popular_links %}
<div class="rounded-lg shadow-md overflow-hidden" style="background-color: {{ item.color }};">
<div class="p-4">
<a href="{% url 'redirect_to_original' item.link.alias %}" class="text-white hover:text-gray-200 font-semibold text-lg block truncate">
{{ item.link.alias }}
</a>
<p class="text-white text-sm mt-2 truncate">{% trans "Clicks" %}: {{ item.link.click_count }}</p>
</div>
</div>
{% empty %}
<p class="text-gray-600 col-span-full">{% trans "No popular links available" %}</p>
{% endfor %}
<!-- Quick Search Section -->
<div class="bg-white rounded-lg shadow-md p-6 mb-6">
<div class="relative">
<input
type="text"
id="main-search-input"
placeholder="{% trans 'Search aliases...' %}"
class="w-full text-lg px-4 py-3 pr-12 rounded-lg border-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
autocomplete="off"
autofocus
>
<div class="absolute inset-y-0 right-0 flex items-center pr-4 pointer-events-none">
<svg class="h-6 w-6 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
<div id="main-search-results" class="absolute top-full left-0 right-0 bg-white border border-gray-300 rounded-b-lg mt-1 hidden z-50 shadow-lg max-h-96 overflow-y-auto"></div>
</div>
</div>
@@ -216,22 +217,24 @@
<!-- Mobile/Tablet Layout (below 1024px) - Original Layout -->
<div class="mobile-layout">
<!-- Popular Links Section -->
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-700 mb-3 uppercase tracking-wide text-sm">{% trans "Popular Links" %}</h2>
<div class="flex flex-row gap-2 sm:grid sm:grid-cols-3 sm:gap-6">
{% for item in popular_links %}
<div class="flex-1 sm:flex-none rounded-lg overflow-hidden shadow-md min-w-0" style="background-color: {{ item.color }};">
<div class="p-2 sm:p-6">
<a href="{% url 'redirect_to_original' item.link.alias %}" class="text-white hover:text-gray-200 font-semibold text-xs sm:text-2xl block truncate leading-tight sm:leading-normal">
{{ item.link.alias }}
</a>
<p class="text-white text-xs sm:text-lg mt-1 sm:mt-3 truncate">{% trans "Clicks" %}: {{ item.link.click_count }}</p>
</div>
<!-- Quick Search Section -->
<div class="mb-4">
<div class="bg-white rounded-lg shadow-md p-3">
<div class="relative">
<input
type="text"
id="main-search-input-mobile"
placeholder="{% trans 'Search aliases...' %}"
class="w-full text-base px-3 py-2 pr-10 rounded-lg border-2 border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
autocomplete="off"
>
<div class="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd" />
</svg>
</div>
{% empty %}
<p class="text-gray-600 w-full">{% trans "No popular links available" %}</p>
{% endfor %}
<div id="main-search-results-mobile" class="absolute top-full left-0 right-0 bg-white border border-gray-300 rounded-b-lg mt-1 hidden z-50 shadow-lg max-h-96 overflow-y-auto"></div>
</div>
</div>
</div>
@@ -538,35 +541,156 @@
const localTime = utcTime.toLocaleString();
element.textContent = localTime;
});
});
// Random image refresh functionality
function refreshRandomImage(img) {
const box = img.getAttribute('data-box');
const timestamp = new Date().getTime();
const width = img.offsetWidth || 500;
const height = img.offsetHeight || 500;
const newUrl = `/api/images/random/${width}/${height}/?fit=crop&v=${timestamp}&box=${box}`;
// Main search functionality - Desktop
setupMainSearch('main-search-input', 'main-search-results');
// Main search functionality - Mobile
setupMainSearch('main-search-input-mobile', 'main-search-results-mobile');
// Create new image to preload
const newImg = new Image();
newImg.onload = function() {
img.style.opacity = '0.5';
setTimeout(() => {
img.src = newUrl;
function setupMainSearch(inputId, resultsId) {
const searchInput = document.getElementById(inputId);
const searchResults = document.getElementById(resultsId);
if (!searchInput || !searchResults) return;
let currentFocus = -1;
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function highlightMatch(text, query) {
if (!query) return text;
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<strong class="text-blue-600 bg-blue-100">$1</strong>');
}
searchInput.addEventListener('input', debounce(function(e) {
const query = e.target.value.trim();
if (query.length === 0) {
searchResults.style.display = 'none';
searchResults.innerHTML = '';
return;
}
const url = `/search/aliases/?q=${encodeURIComponent(query)}`;
fetch(url)
.then(response => response.json())
.then(data => {
searchResults.innerHTML = '';
currentFocus = -1;
if (Array.isArray(data) && data.length > 0) {
data.forEach(item => {
if (item && typeof item === 'object' && 'alias' in item && 'url' in item) {
const div = document.createElement('div');
div.className = 'search-result-item p-3 hover:bg-gray-100 cursor-pointer border-b border-gray-100 last:border-b-0';
const aliasLink = document.createElement('a');
aliasLink.href = item.url;
aliasLink.innerHTML = highlightMatch(item.alias, query);
aliasLink.className = 'text-blue-600 hover:underline block text-lg';
div.appendChild(aliasLink);
searchResults.appendChild(div);
}
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div class="p-3 text-gray-500">{% trans "No results found" %}</div>';
searchResults.style.display = 'block';
}
})
.catch(error => {
console.error('Error:', error);
searchResults.style.display = 'none';
});
}, 300));
searchInput.addEventListener('keydown', function(e) {
let items = searchResults.getElementsByClassName("search-result-item");
if (e.keyCode == 40) { // Down arrow
e.preventDefault();
currentFocus++;
addActive(items);
} else if (e.keyCode == 38) { // Up arrow
e.preventDefault();
currentFocus--;
addActive(items);
} else if (e.keyCode == 13) { // Enter
e.preventDefault();
if (currentFocus > -1 && items[currentFocus]) {
items[currentFocus].querySelector('a').click();
} else if (items.length > 0) {
// Go to first result if no selection
items[0].querySelector('a').click();
}
} else if (e.keyCode == 27) { // Escape
searchResults.style.display = 'none';
searchResults.innerHTML = '';
searchInput.value = '';
}
});
function addActive(items) {
if (!items || items.length === 0) return false;
removeActive(items);
if (currentFocus >= items.length) currentFocus = 0;
if (currentFocus < 0) currentFocus = (items.length - 1);
items[currentFocus].classList.add("bg-blue-50");
}
function removeActive(items) {
for (let i = 0; i < items.length; i++) {
items[i].classList.remove("bg-blue-50");
}
}
// Close dropdown when clicking outside
document.addEventListener('click', function(e) {
if (e.target !== searchInput && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
// Random image refresh functionality
window.refreshRandomImage = function(img) {
const box = img.getAttribute('data-box');
const timestamp = new Date().getTime();
const width = img.offsetWidth || 500;
const height = img.offsetHeight || 500;
const newUrl = `/api/images/random/${width}/${height}/?fit=crop&v=${timestamp}&box=${box}`;
const newImg = new Image();
newImg.onload = function() {
img.style.opacity = '0.5';
setTimeout(() => {
img.src = newUrl;
img.style.opacity = '1';
}, 150);
};
newImg.onerror = function() {
console.error('Failed to load new random image');
const currentUrl = new URL(img.src);
currentUrl.searchParams.set('v', timestamp);
img.src = currentUrl.toString();
img.style.opacity = '1';
}, 150);
};
newImg.src = newUrl;
};
newImg.onerror = function() {
console.error('Failed to load new random image');
// Fallback - just update timestamp on current URL
const currentUrl = new URL(img.src);
currentUrl.searchParams.set('v', timestamp);
img.src = currentUrl.toString();
img.style.opacity = '1';
};
newImg.src = newUrl;
}
});
// Select all functionality
document.getElementById('select-all').addEventListener('change', function() {
-7
View File
@@ -82,13 +82,6 @@ class LinkListView(ListView):
'#1abc9c', '#d35400', '#34495e', '#16a085', '#27ae60'
]
# Add top 3 popular links to the context with random colors
popular_links = Link.objects.order_by('-click_count')[:3]
context['popular_links'] = [
{'link': link, 'color': random.choice(colors)}
for link in popular_links
]
# Add latest 3 posts to the context (most recent first)
context['latest_posts'] = Post.objects.all().order_by('-created_at')[:3]