Update UI

This commit is contained in:
2026-07-29 11:57:12 +10:00
parent 361bd93788
commit b1acb24d81
4 changed files with 247 additions and 159 deletions
+54 -27
View File
@@ -325,6 +325,17 @@ def get_random_image_hierarchical(base_path: Path) -> Optional[Path]:
Only selects from subfolders, excludes root directory.
Assumes flat structure within subfolders (no nested subdirectories).
Robustness notes:
- Candidates are validated during the scan (not after picking), so a folder
whose first few files are invalid (tiny/corrupt/oversized) doesn't go
permanently dark — directory iteration order is stable, so validating
only the picked candidate used to fail the same way on every attempt.
- If a full pass finds nothing because every valid candidate is in the
recently-served queue, a second pass ignores the queue. Recency
avoidance is best-effort and must never cause a hard 404: the queue only
rotates on success, so without this fallback a run of failures froze the
queue and the API degraded until process restart.
"""
try:
# Get only subdirectories (exclude root directory)
@@ -337,45 +348,61 @@ def get_random_image_hierarchical(base_path: Path) -> Optional[Path]:
logger.warning("No subfolders found in images directory")
return None
# Try up to 20 random directories
for attempt in range(min(20, len(directories))):
random_dir = random.choice(directories)
for ignore_recent in (False, True):
# Try up to 20 random directories per pass
for attempt in range(min(20, len(directories))):
random_dir = random.choice(directories)
# Quick check if directory has images (flat structure only)
image_files = []
file_count = 0
# Quick check if directory has images (flat structure only)
image_files = []
file_count = 0
for item in random_dir.iterdir():
# Skip subdirectories - only process files
if not item.is_file():
continue
for item in random_dir.iterdir():
# Skip subdirectories - only process files
if not item.is_file():
continue
file_count += 1
if file_count > MAX_DIRECTORY_SCAN:
logger.warning(f"Directory {random_dir.name} has too many files, skipping")
break
if item.suffix.lower() in SUPPORTED_FORMATS:
# Skip recently served images
with recent_images_lock:
if item.name not in recent_images_queue:
image_files.append(item)
# Early termination: if we found some images, we can select randomly
if len(image_files) >= 5:
file_count += 1
if file_count > MAX_DIRECTORY_SCAN:
logger.warning(f"Directory {random_dir.name} has too many files, skipping")
break
if image_files:
selected_file = random.choice(image_files)
if is_valid_image_fast(selected_file):
if item.suffix.lower() in SUPPORTED_FORMATS:
# Skip recently served images (unless we're on the
# fallback pass that ignores the recency queue)
if not ignore_recent:
with recent_images_lock:
if item.name in recent_images_queue:
continue
# Keep only candidates that pass validation, so an
# unlucky pick can't waste the whole attempt
if is_valid_image_fast(item):
image_files.append(item)
# Early termination: if we found some images, we can select randomly
if len(image_files) >= 5:
break
if image_files:
selected_file = random.choice(image_files)
# Add to recent images queue
with recent_images_lock:
recent_images_queue.append(selected_file.name)
logger.debug(f"Hierarchical selection: folder='{random_dir.name}', "
f"file='{selected_file.name}', candidates={len(image_files)}")
f"file='{selected_file.name}', candidates={len(image_files)}, "
f"ignore_recent={ignore_recent}")
return selected_file
if ignore_recent:
break
with recent_images_lock:
had_recent = len(recent_images_queue) > 0
if not had_recent:
break
logger.info("First pass found no non-recent images; retrying without the recency filter")
logger.warning("No valid images found in any subfolder after 20 attempts")
return None
+189 -129
View File
@@ -56,19 +56,22 @@
opacity: 1 !important;
transform: none !important;
}
.app-tile, .post-card, .fan-card, .fan-card img, .fan-card-frame, .fan-lightbox { transition: none !important; animation: none !important; }
.app-chip, .post-row, .fan-card, .fan-card img, .fan-card-frame, .fan-lightbox { transition: none !important; animation: none !important; }
}
/* ---------- Hero ---------- */
/* ---------- Hero (compact: search + stats share one row on desktop) ---------- */
.apple-hero {
position: relative;
text-align: center;
padding: clamp(2.75rem, 8vh, 5.5rem) 1rem clamp(2.5rem, 6vh, 4rem);
padding: clamp(1.5rem, 4vh, 2.5rem) 1rem clamp(1.25rem, 3vh, 1.75rem);
}
@media (min-width: 768px) {
.apple-hero { display: flex; align-items: center; gap: 2.25rem; text-align: left; }
}
.hero-glow {
position: absolute;
inset: -12% -6% auto;
height: 460px;
height: 340px;
z-index: 0;
pointer-events: none;
background:
@@ -100,7 +103,10 @@
}
/* ---------- Hero search (frosted glass pill) ---------- */
.hero-search { position: relative; max-width: 40rem; margin: 2.6rem auto 0; text-align: left; }
.hero-search { position: relative; width: 100%; max-width: 44rem; margin: 0 auto; text-align: left; }
@media (min-width: 768px) {
.hero-search { flex: 1 1 auto; margin: 0; }
}
/* Must stack above .hero-stats (which shares the same z-index: 1 from the
">*:not(.hero-glow)" rule above) so the search results dropdown never
renders underneath the stats row. */
@@ -110,7 +116,7 @@
width: 1.25rem; height: 1.25rem; color: var(--apple-gray); pointer-events: none;
}
.hero-search input {
width: 100%; height: 3.8rem; padding: 0 1.6rem 0 3.5rem;
width: 100%; height: 3.4rem; padding: 0 1.6rem 0 3.5rem;
font-size: 1.08rem; color: var(--apple-text);
background: rgba(255, 255, 255, 0.82);
-webkit-backdrop-filter: blur(20px) saturate(180%);
@@ -154,14 +160,17 @@
.search-empty { padding: 1.1rem; color: var(--apple-gray); text-align: center; font-size: 0.95rem; }
/* ---------- Hero stats ---------- */
.hero-stats { display: flex; align-items: center; justify-content: center; gap: 1.8rem; margin-top: 2.5rem; }
.hero-stat { display: flex; flex-direction: column; align-items: center; gap: 0.15rem; }
.hero-stat-number { font-size: 1.65rem; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.hero-stat-label { font-size: 0.76rem; color: var(--apple-gray); text-transform: uppercase; letter-spacing: 0.09em; font-weight: 500; }
.hero-stat-divider { width: 1px; height: 2.3rem; background: rgba(0, 0, 0, 0.10); }
.hero-stats { display: flex; align-items: center; justify-content: center; gap: 1.4rem; margin-top: 1.1rem; }
@media (min-width: 768px) {
.hero-stats { margin-top: 0; flex: none; padding-right: 0.35rem; }
}
.hero-stat { display: flex; flex-direction: column; align-items: center; gap: 0.1rem; }
.hero-stat-number { font-size: 1.35rem; font-weight: 700; letter-spacing: -0.02em; font-variant-numeric: tabular-nums; }
.hero-stat-label { font-size: 0.7rem; color: var(--apple-gray); text-transform: uppercase; letter-spacing: 0.09em; font-weight: 500; }
.hero-stat-divider { width: 1px; height: 2rem; background: rgba(0, 0, 0, 0.10); }
/* ---------- Sections, cards, buttons ---------- */
.home-section { margin-top: clamp(2.5rem, 6vh, 4.5rem); }
.home-section { margin-top: clamp(1.5rem, 3.5vh, 2.5rem); }
.apple-card {
background: #fff;
border-radius: var(--apple-radius-lg);
@@ -276,62 +285,96 @@
.apple-btn-secondary:hover { background: #ececef; }
.apple-btn-secondary[disabled] { opacity: 0.55; cursor: default; pointer-events: none; }
/* ---------- Posts ---------- */
.posts-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.1rem; list-style: none; margin: 0; padding: 0; }
.post-card {
background: #fff; border-radius: 24px;
border: 1px solid rgba(0, 0, 0, 0.04);
box-shadow: var(--apple-shadow-card);
transition: transform 0.45s var(--apple-ease), box-shadow 0.45s var(--apple-ease);
/* ---------- First-screen dashboard (posts + images side by side) ---------- */
.dash-grid {
display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem;
align-items: start;
}
.post-card:hover { transform: translateY(-5px); box-shadow: var(--apple-shadow-lift); }
.post-card-link { display: flex; flex-direction: column; gap: 0.55rem; height: 100%; padding: 1.5rem 1.5rem 1.4rem; color: inherit; text-decoration: none; }
.post-card-date { font-size: 0.78rem; color: var(--apple-gray); font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; }
.post-card-title {
font-size: 1.13rem; font-weight: 700; letter-spacing: -0.012em; line-height: 1.32;
display: -webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
@media (min-width: 1024px) {
.dash-grid { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
}
.post-card-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: auto; padding-top: 0.4rem; }
.tag-pill { font-size: 0.75rem; font-weight: 500; color: var(--apple-gray); background: rgba(0, 0, 0, 0.05); padding: 0.22rem 0.65rem; border-radius: 980px; }
.dash-rail { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
/* ---------- Latest Posts (dense list) ---------- */
.posts-panel { overflow: hidden; }
.posts-panel-header {
display: flex; align-items: baseline; justify-content: space-between; gap: 1rem;
padding: 1.35rem 1.5rem 0.8rem;
}
.post-list { list-style: none; margin: 0; padding: 0.15rem 0.5rem 0.6rem; }
.post-row {
display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center; gap: 0.9rem;
padding: 0.72rem 1rem; border-radius: 16px;
color: inherit; text-decoration: none;
transition: background 0.25s ease;
}
.post-row:hover { background: rgba(0, 0, 0, 0.035); }
.post-row-date {
font-size: 0.78rem; font-weight: 600; color: var(--apple-gray);
text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap;
}
.post-row-main { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.post-row-title {
font-size: 0.98rem; font-weight: 600; letter-spacing: -0.01em;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.post-row-summary {
font-size: 0.82rem; color: var(--apple-gray);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.post-row-tags { display: flex; gap: 0.35rem; }
.post-row-chev { width: 0.9rem; height: 0.9rem; color: #c7c7cc; flex: none; transition: transform 0.3s var(--apple-ease); }
.post-row:hover .post-row-chev { transform: translateX(2px); }
.tag-pill { font-size: 0.75rem; font-weight: 500; color: var(--apple-gray); background: rgba(0, 0, 0, 0.05); padding: 0.22rem 0.65rem; border-radius: 980px; white-space: nowrap; }
.empty-state { padding: 2.75rem 1.5rem; text-align: center; color: var(--apple-gray); display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.empty-state svg { width: 2.6rem; height: 2.6rem; color: #c7c7cc; }
/* ---------- Bento tiles (mini apps) ---------- */
.tile-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); gap: 1.1rem; }
.app-tile {
/* ---------- Mini app chips (compact gradients under the photo fan) ---------- */
.app-chips { display: grid; grid-template-columns: 1fr 1fr; gap: 0.9rem; }
.app-chip {
position: relative; isolation: isolate; overflow: hidden;
display: flex; flex-direction: column; justify-content: flex-end;
min-height: 200px; padding: 1.7rem; border-radius: var(--apple-radius-lg);
display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
min-height: 68px; padding: 0.9rem 1.15rem; border-radius: var(--apple-radius-md);
color: #fff; text-decoration: none; box-shadow: var(--apple-shadow-card);
transition: transform 0.45s var(--apple-ease), box-shadow 0.45s var(--apple-ease);
}
.app-tile:hover { transform: translateY(-5px); box-shadow: var(--apple-shadow-lift); color: #fff; }
.app-tile::before {
.app-chip:hover { transform: translateY(-3px); box-shadow: var(--apple-shadow-lift); color: #fff; }
.app-chip::before {
content: ""; position: absolute; inset: 0; z-index: -1;
background: radial-gradient(120% 120% at 85% 0%, rgba(255, 255, 255, 0.30) 0%, rgba(255, 255, 255, 0) 46%);
}
.tile-arrow {
position: absolute; top: 1.15rem; right: 1.15rem; width: 34px; height: 34px;
border-radius: 50%; background: rgba(255, 255, 255, 0.22);
display: flex; align-items: center; justify-content: center;
transition: background 0.3s ease, transform 0.35s var(--apple-ease);
.app-chip > svg { width: 1rem; height: 1rem; flex: none; opacity: 0.9; transition: transform 0.35s var(--apple-ease); }
.app-chip:hover > svg { transform: translate(2px, -2px); }
.app-chip-text { display: flex; flex-direction: column; gap: 0.05rem; min-width: 0; }
.app-chip-name { font-size: 1.02rem; font-weight: 700; letter-spacing: -0.01em; }
.app-chip-desc {
font-size: 0.8rem; color: rgba(255, 255, 255, 0.85);
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tile-arrow svg { width: 1rem; height: 1rem; }
.app-tile:hover .tile-arrow { background: rgba(255, 255, 255, 0.36); transform: translate(2px, -2px); }
.app-tile-name { font-size: 1.45rem; font-weight: 700; letter-spacing: -0.015em; }
.app-tile-desc { font-size: 0.95rem; color: rgba(255, 255, 255, 0.86); margin-top: 0.15rem; }
.gradient-1 { background: linear-gradient(135deg, #0a84ff 0%, #5e5ce6 100%); }
.gradient-2 { background: linear-gradient(135deg, #ff9f0a 0%, #ff375f 100%); }
.gradient-3 { background: linear-gradient(135deg, #30d158 0%, #0a84ff 100%); }
.gradient-4 { background: linear-gradient(135deg, #bf5af2 0%, #ff375f 100%); }
/* ---------- Photo fan (creative image deck) ---------- */
.photo-fan-section { margin-top: clamp(2rem, 5vh, 3.5rem); }
.fan-actions { display: flex; align-items: center; gap: 1.1rem; }
.photo-fan {
display: flex; justify-content: center; align-items: flex-end;
padding: clamp(1.75rem, 4.5vw, 2.75rem) 0.5rem 1.25rem;
padding: clamp(1.25rem, 3vw, 1.75rem) 0.5rem 0.75rem;
}
/* Fan lives in the dashboard's side rail on desktop: shrink the cards so
all five fit the narrower column. When the grid stacks (< 1024px) the
fan is full-width again and falls back to the original roomy sizes. */
.rail-header { flex-wrap: wrap; margin-bottom: 0; padding: 0; }
.dash-rail .fan-card { --w: clamp(120px, 11vw, 170px); }
@media (max-width: 1023px) {
.dash-rail .fan-card { --w: clamp(96px, 15.5vw, 172px); }
.rail-header { padding: 0 0.35rem; margin-bottom: 0.5rem; }
}
@media (max-width: 640px) {
.dash-rail .fan-card { --w: clamp(84px, 21vw, 120px); }
}
.fan-card {
--w: clamp(96px, 15.5vw, 172px);
@@ -438,6 +481,11 @@
.library-footer { padding: 1rem 1.1rem 1.25rem; }
.apple-table thead th, .apple-table tbody td { padding-left: 0.8rem; padding-right: 0.8rem; }
.hero-stats { gap: 1.25rem; }
.posts-panel-header { padding: 1.15rem 1.1rem 0.6rem; }
.post-row { gap: 0.65rem; padding: 0.68rem 0.75rem; }
.post-row-tags { display: none; }
.app-chip-desc { display: none; }
.app-chip { min-height: 52px; }
}
</style>
{% endblock %}
@@ -531,26 +579,77 @@
</div>
</section>
<!-- ============ Photo Fan (Random Images) ============ -->
<section class="photo-fan-section" aria-labelledby="images-heading">
<div class="section-header">
<h2 id="images-heading" class="section-title">{% trans "Random Images" %}</h2>
<div class="fan-actions">
<button type="button" id="shuffle-all-images" class="apple-btn apple-btn-secondary">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
{% trans "Shuffle all" %}
</button>
<a href="{% url 'mini-apps-image-gallery' %}" class="apple-link">
{% trans "Open gallery" %}
<!-- ============ First-screen dashboard: Latest Posts + Random Images ============ -->
<section class="dash-grid home-section">
<!-- Latest Posts (dense list) -->
<div class="apple-card posts-panel reveal" aria-labelledby="posts-heading">
<div class="posts-panel-header">
<h2 id="posts-heading" class="section-title">{% trans "Latest Posts" %}</h2>
<a href="{% url 'post-list' %}" class="apple-link">
{% trans "View all posts" %}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
{% if latest_posts %}
<ul class="post-list" role="list">
{% for post in latest_posts %}
<li>
<a href="{{ post.get_absolute_url }}" class="post-row">
<span class="post-row-date">{{ post.created_at|date:"M d" }}</span>
<span class="post-row-main">
<span class="post-row-title">{{ post.title }}</span>
{% if post.summary %}
<span class="post-row-summary">{{ post.summary }}</span>
{% endif %}
</span>
{% if post.tags.all %}
<span class="post-row-tags">
{% for tag in post.tags.all|slice:":2" %}
<span class="tag-pill">{{ tag.name }}</span>
{% endfor %}
</span>
{% endif %}
<svg class="post-row-chev" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2"/>
</svg>
<p>{% trans "No posts available yet" %}</p>
<a href="{% url 'post-create' %}" class="apple-btn apple-btn-primary">{% trans "Create your first post" %}</a>
</div>
{% endif %}
</div>
<div class="photo-fan">
<!-- Side rail: photo fan + mini app chips -->
<div class="dash-rail">
<section aria-labelledby="images-heading">
<div class="section-header rail-header">
<h2 id="images-heading" class="section-title">{% trans "Random Images" %}</h2>
<div class="fan-actions">
<button type="button" id="shuffle-all-images" class="apple-btn apple-btn-secondary">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
{% trans "Shuffle all" %}
</button>
<a href="{% url 'mini-apps-image-gallery' %}" class="apple-link">
{% trans "Open gallery" %}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
</div>
<div class="photo-fan">
<button type="button" class="fan-card" style="--r: -13deg; --fy: 18px; --z: 1; --d: 0; --tape: rgba(0, 113, 227, 0.22); --tape-r: -4deg;" onclick="openFanLightbox(this.querySelector('img'))" aria-label="{% trans 'View Full Size' %}">
<span class="fan-card-frame">
<img src="/api/images/random/600/800/?fit=crop&v={{ timestamp }}&box=fan1" data-box="fan1" alt="{% trans 'Random Image' %}" loading="lazy" onerror="handleRandomImageError(this)">
@@ -601,8 +700,36 @@
</span>
</span>
</button>
</div>
</section>
</div>
</section>
<!-- Mini app chips -->
{% if mini_apps %}
<div class="app-chips">
{% for app in mini_apps %}
{% if app.url != '#' %}
<a href="{% url app.url %}" class="app-chip gradient-{% cycle '1' '2' '3' '4' %}" aria-label="{{ app.name }} — {{ app.description }}">
<span class="app-chip-text">
<span class="app-chip-name">{{ app.name }}</span>
<span class="app-chip-desc">{{ app.description }}</span>
</span>
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 17L17 7M17 7H8M17 7v9"/>
</svg>
</a>
{% else %}
<div class="app-chip gradient-{% cycle '1' '2' '3' '4' %}">
<span class="app-chip-text">
<span class="app-chip-name">{{ app.name }}</span>
<span class="app-chip-desc">{{ app.description }}</span>
</span>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div><!-- /.dash-rail -->
</section><!-- /.dash-grid -->
<!-- ============ Fan Lightbox (full-size preview) ============ -->
<div id="fan-lightbox" class="fan-lightbox" hidden role="dialog" aria-modal="true" aria-label="{% trans 'Random Image' %}">
@@ -724,74 +851,7 @@
</form>
</section>
<!-- ============ Latest Posts ============ -->
<section class="home-section reveal" aria-labelledby="posts-heading">
<div class="section-header">
<h2 id="posts-heading" class="section-title">{% trans "Latest Posts" %}</h2>
<a href="{% url 'post-list' %}" class="apple-link">
{% trans "View all posts" %}
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
</svg>
</a>
</div>
{% if latest_posts %}
<ul class="posts-grid" role="list">
{% for post in latest_posts %}
<li class="post-card reveal" style="--d: {{ forloop.counter0 }};">
<a href="{{ post.get_absolute_url }}" class="post-card-link">
<span class="post-card-date">{{ post.created_at|date:"M d, Y" }}</span>
<span class="post-card-title">{{ post.title }}</span>
{% if post.tags.all %}
<span class="post-card-tags">
{% for tag in post.tags.all|slice:":2" %}
<span class="tag-pill">{{ tag.name }}</span>
{% endfor %}
</span>
{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="apple-card empty-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9.5a2 2 0 00-2-2h-2"/>
</svg>
<p>{% trans "No posts available yet" %}</p>
<a href="{% url 'post-create' %}" class="apple-btn apple-btn-primary">{% trans "Create your first post" %}</a>
</div>
{% endif %}
</section>
<!-- ============ Pinned Mini Apps ============ -->
<section class="home-section reveal" aria-labelledby="apps-heading" style="margin-bottom: 3.5rem;">
<div class="section-header">
<h2 id="apps-heading" class="section-title">{% trans "Pinned Mini Apps" %}</h2>
</div>
<div class="tile-grid">
{% for app in mini_apps %}
{% if app.url != '#' %}
<a href="{% url app.url %}" class="app-tile gradient-{% cycle '1' '2' '3' '4' %} reveal" style="--d: {{ forloop.counter0 }};" aria-label="{{ app.name }} — {{ app.description }}">
<span class="tile-arrow" aria-hidden="true">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M7 17L17 7M17 7H8M17 7v9"/>
</svg>
</span>
<span class="app-tile-name">{{ app.name }}</span>
<span class="app-tile-desc">{{ app.description }}</span>
</a>
{% else %}
<div class="app-tile gradient-{% cycle '1' '2' '3' '4' %} reveal" style="--d: {{ forloop.counter0 }};">
<span class="app-tile-name">{{ app.name }}</span>
<span class="app-tile-desc">{{ app.description }}</span>
</div>
{% endif %}
{% empty %}
<p style="color: var(--apple-gray);">{% trans "No mini apps available" %}</p>
{% endfor %}
</div>
</section>
<div style="height: 2.5rem;" aria-hidden="true"></div>
</div>
{% endblock %}
+3 -2
View File
@@ -88,8 +88,9 @@ class LinkListView(ListView):
'#1abc9c', '#d35400', '#34495e', '#16a085', '#27ae60'
]
# Add latest 3 posts to the context (most recent first)
context['latest_posts'] = Post.objects.all().order_by('-created_at')[:3]
# Add latest 5 posts to the context (most recent first), shown as a
# dense list in the first-screen dashboard on the home page.
context['latest_posts'] = Post.objects.prefetch_related('tags').order_by('-created_at')[:5]
# Add pinned mini apps with random colors
mini_apps = [
+1 -1
View File
@@ -56,7 +56,7 @@
<script src="{% static 'js/common.js' %}"></script>
</head>
<body class="bg-gray-100">
<nav id="main-nav" class="bg-custom-blue text-white p-4 fixed w-full top-0 z-10 nav-visible">
<nav id="main-nav" class="bg-custom-blue text-white p-4 fixed w-full top-0 z-50 nav-visible">
<div class="container mx-auto flex justify-between items-center">
<a href="{% url 'link_list' %}" class="text-xl font-bold">{% trans "GoLinks" %}</a>
<div class="flex items-center space-x-4">