diff --git a/links/search_views.py b/links/search_views.py index 64e2e4d..9533960 100644 --- a/links/search_views.py +++ b/links/search_views.py @@ -8,6 +8,7 @@ from django.db import models from django.utils.text import slugify from .models import Link, Page, Post, Bookmark from .search_backend import search_backend +from django.urls import reverse from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt from django.core.cache import cache @@ -243,12 +244,40 @@ def search_api_v2(request): def search_aliases(request): query = request.GET.get('q', '').strip() - links = Link.objects.filter(alias__icontains=query).annotate(priority=Case(When(alias__iexact=query, then=Value(1)), When(alias__istartswith=query, then=Value(2)), default=Value(3), output_field=IntegerField())).order_by('priority', 'alias')[:10] + qs = Link.objects.all() + if query: + qs = qs.filter( + Q(alias__icontains=query) + | Q(original_url__icontains=query) + | Q(description__icontains=query) + | Q(text__icontains=query) + | Q(tags__name__icontains=query) + ).distinct() + qs = qs.annotate(priority=Case( + When(alias__iexact=query, then=Value(1)), + When(alias__istartswith=query, then=Value(2)), + default=Value(3), output_field=IntegerField(), + )).order_by('priority', 'alias') + else: + # No query: return the most recently updated links (used by the + # home-page library as the "recent links" default view). + qs = qs.order_by('-updated_at') + links = qs[:20] results = [] for link in links: try: url = request.build_absolute_uri(f'/{link.alias}/') - results.append({'alias': link.alias, 'url': url}) + results.append({ + 'alias': link.alias, + 'url': url, + 'id': link.id, + 'link_type': link.link_type, + 'detail_url': reverse('link_detail', args=[link.id]), + 'edit_url': reverse('link_update', args=[link.id]), + 'click_count': link.click_count, + 'description': (link.description or '')[:160], + 'original_url': (link.original_url or '')[:180], + }) except Exception as e: logger.error(f"Error creating URL for alias {link.alias}: {str(e)}") continue diff --git a/links/templates/links/link_list.html b/links/templates/links/link_list.html index a078a63..a021d2c 100644 --- a/links/templates/links/link_list.html +++ b/links/templates/links/link_list.html @@ -70,7 +70,7 @@ } .hero-glow { position: absolute; - inset: -12% -6% auto; + inset: -12% 0 auto; height: 340px; z-index: 0; pointer-events: none; @@ -286,6 +286,70 @@ .apple-btn-secondary:hover { background: #ececef; } .apple-btn-secondary[disabled] { opacity: 0.55; cursor: default; pointer-events: none; } + /* ---------- Search-driven result cards (replace the table while searching) ---------- */ + .search-card-list { + display: flex; flex-direction: column; gap: 0.7rem; + padding: 0.2rem 1.8rem 1.4rem; + } + .search-card { + display: flex; align-items: center; gap: 1rem; + background: #fff; border: 1px solid rgba(0, 0, 0, 0.04); + border-radius: 20px; box-shadow: var(--apple-shadow-card); + padding: 0.85rem 1.15rem; + transition: box-shadow 0.3s var(--apple-ease), transform 0.3s var(--apple-ease); + } + .search-card:hover { box-shadow: var(--apple-shadow-lift); transform: translateY(-1px); } + .search-card-main { flex: 1; min-width: 0; cursor: pointer; border-radius: 12px; } + .search-card-main:focus-visible { outline: 2px solid var(--apple-blue); outline-offset: 3px; } + .search-card-title { display: flex; align-items: center; gap: 0.55rem; min-width: 0; } + .search-card-title a.alias-link { + font-size: 1.08rem; font-weight: 600; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .search-card-title a.alias-link:hover { text-decoration: underline; } + .search-card-url { + font-size: 0.82rem; color: var(--apple-gray); margin-top: 0.15rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .search-card-desc { + font-size: 0.85rem; color: var(--apple-gray); margin-top: 0.2rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .search-card mark { + background: rgba(0, 113, 227, 0.13); color: var(--apple-blue); + border-radius: 4px; padding: 0 1px; + } + .search-card-count { + font-size: 0.72rem; color: #c7c7cc; font-variant-numeric: tabular-nums; + flex: none; white-space: nowrap; + } + .search-card-actions { display: flex; align-items: center; gap: 0.2rem; flex: none; } + .search-empty-card { padding: 2.75rem 1.5rem; text-align: center; color: var(--apple-gray); } + + /* ---------- Mobile: table rows become cards (kills the 620px horizontal scroll) ---------- */ + @media (max-width: 640px) { + .table-scroll { overflow: visible; } + .apple-table { min-width: 0; } + .apple-table thead { display: none; } + .apple-table tbody { display: flex; flex-direction: column; gap: 0.7rem; padding: 0 0.85rem 0.85rem; } + .apple-table tbody tr { + display: flex; align-items: center; gap: 0.8rem; + background: #fff; border-radius: 18px; + box-shadow: var(--apple-shadow-card); + padding: 0.8rem 1rem; + } + .apple-table tbody td { display: block; padding: 0; border-bottom: none !important; } + /* Tailwind's `hidden` helper must win over the block layout above, or the + type/url/clicks cells reappear on mobile and blow the card wider than + the viewport (verified: 564px row on a 390px screen). */ + .apple-table tbody td.hidden { display: none !important; } + .apple-table tbody td:first-child { flex: none; } + .apple-table tbody td:nth-child(2) { flex: 1; min-width: 0; } + .apple-table tbody td:last-child { flex: none; } + /* Empty / no-results rows (colspan) span the whole card width */ + .apple-table tbody tr td[colspan] { flex: 1; text-align: center; padding: 2.25rem 1rem; } + } + /* ---------- First-screen dashboard (posts + images side by side) ---------- */ .dash-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem; @@ -585,7 +649,6 @@ id="main-search-input" placeholder="{% trans 'Search aliases...' %}" autocomplete="off" - autofocus role="combobox" aria-expanded="false" aria-controls="main-search-results" @@ -796,7 +859,9 @@ -