updated tandoor widget with swiping
This commit is contained in:
@@ -791,55 +791,135 @@ data:
|
||||
<style>
|
||||
.mealwrap { display:flex; flex-direction:column; gap:10px; }
|
||||
.mealmeta { opacity:.65; font-size:12px; display:flex; justify-content:space-between; align-items:center; }
|
||||
.mealscroller { display:flex; gap:12px; overflow-x:auto; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; padding-bottom:6px; }
|
||||
.mealslide { min-width:100%; scroll-snap-align:start; border-radius:14px; overflow:hidden; background:rgba(255,255,255,0.04); box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset; }
|
||||
.mealimg { height:150px; background:rgba(0,0,0,0.15); display:flex; align-items:center; justify-content:center; overflow:hidden; }
|
||||
.mealscroller-wrap { position:relative; }
|
||||
.mealscroller { display:flex; gap:0; overflow-x:auto; scroll-snap-type:x mandatory; -webkit-overflow-scrolling:touch; scrollbar-width:none; -ms-overflow-style:none; }
|
||||
.mealscroller::-webkit-scrollbar { display:none; }
|
||||
.mealslide { min-width:100%; flex-shrink:0; scroll-snap-align:start; border-radius:14px; overflow:hidden; background:rgba(255,255,255,0.04); box-shadow:0 0 0 1px rgba(255,255,255,0.06) inset; }
|
||||
.mealimg { height:150px; background:rgba(0,0,0,0.15); display:flex; align-items:center; justify-content:center; overflow:hidden; position:relative; }
|
||||
.mealimg img { width:100%; height:100%; object-fit:cover; display:block; }
|
||||
.mealnoimg { opacity:.55; font-size:12px; padding:18px; text-align:center; }
|
||||
.mealname { padding:10px 12px 8px; font-weight:700; opacity:.95; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
.mealactions { padding:0 12px 12px; display:flex; gap:10px; opacity:.8; font-size:12px; }
|
||||
.mealactions a { text-decoration:none; }
|
||||
.meallink { display:block; color:inherit; text-decoration:none; }
|
||||
/* Navigation arrows */
|
||||
.meal-nav { position:absolute; top:50%; transform:translateY(-50%); width:32px; height:32px; border-radius:50%; background:rgba(0,0,0,0.5); color:#fff; border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:14px; z-index:10; opacity:0.7; transition:opacity 0.2s; }
|
||||
.meal-nav:hover { opacity:1; background:rgba(0,0,0,0.7); }
|
||||
.meal-nav.prev { left:8px; }
|
||||
.meal-nav.next { right:8px; }
|
||||
.meal-nav.hidden { display:none; }
|
||||
/* Pagination dots */
|
||||
.meal-dots { display:flex; justify-content:center; gap:6px; padding:8px 0 4px; }
|
||||
.meal-dot { width:8px; height:8px; border-radius:50%; background:rgba(255,255,255,0.25); cursor:pointer; transition:background 0.2s; }
|
||||
.meal-dot.active { background:rgba(255,255,255,0.8); }
|
||||
.meal-dot:hover { background:rgba(255,255,255,0.5); }
|
||||
</style>
|
||||
|
||||
{{ $tandoor := .Options.StringOr "tandoor_url" "https://tandoor.dooplex.hu" }}
|
||||
{{ $items := .JSON.Array "items" }}
|
||||
{{ $total := .JSON.Int "total_recipes" }}
|
||||
{{ $date := .JSON.String "date" }}
|
||||
{{ $count := len $items }}
|
||||
|
||||
<div class="mealwrap">
|
||||
<div class="mealmeta">
|
||||
<span>Today’s picks ({{ $total }} total)</span>
|
||||
<span>Today's picks ({{ $count }} total)</span>
|
||||
<a href="{{ $tandoor }}" target="_blank" rel="noreferrer">Open Tandoor</a>
|
||||
</div>
|
||||
|
||||
{{ if lt (len $items) 1 }}
|
||||
{{ if lt $count 1 }}
|
||||
<div class="color-negative">No recipes returned.</div>
|
||||
{{ else }}
|
||||
<div class="mealscroller">
|
||||
{{ range $i, $r := $items }}
|
||||
{{ $name := $r.String "name" }}
|
||||
{{ $img := $r.String "image" }}
|
||||
{{ $url := $r.String "url" }}
|
||||
{{ $cook := $r.String "cook_url" }}
|
||||
<div class="mealscroller-wrap">
|
||||
<div class="mealscroller" id="mealScroller">
|
||||
{{ range $i, $r := $items }}
|
||||
{{ $name := $r.String "name" }}
|
||||
{{ $img := $r.String "image" }}
|
||||
{{ $url := $r.String "url" }}
|
||||
{{ $cook := $r.String "cook_url" }}
|
||||
|
||||
<div class="mealslide">
|
||||
<a class="meallink" href="{{ $url }}" target="_blank" rel="noreferrer">
|
||||
<div class="mealimg">
|
||||
{{ if $img }}<img src="{{ $img }}" alt="" />{{ else }}<div class="mealnoimg">No image</div>{{ end }}
|
||||
<div class="mealslide" data-index="{{ $i }}">
|
||||
<a class="meallink" href="{{ $url }}" target="_blank" rel="noreferrer">
|
||||
<div class="mealimg">
|
||||
{{ if $img }}<img src="{{ $img }}" alt="" loading="lazy" />{{ else }}<div class="mealnoimg">No image</div>{{ end }}
|
||||
</div>
|
||||
<div class="mealname">{{ $name }}</div>
|
||||
</a>
|
||||
<div class="mealactions">
|
||||
<a href="{{ $url }}" target="_blank" rel="noreferrer">Open</a>
|
||||
<a href="{{ $cook }}" target="_blank" rel="noreferrer">Cooked today ✔</a>
|
||||
</div>
|
||||
<div class="mealname">{{ $name }}</div>
|
||||
</a>
|
||||
<div class="mealactions">
|
||||
<a href="{{ $url }}" target="_blank" rel="noreferrer">Open</a>
|
||||
<a href="{{ $cook }}" target="_blank" rel="noreferrer">Cooked today ✓</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if gt $count 1 }}
|
||||
<button class="meal-nav prev" onclick="mealNav(-1)" aria-label="Previous">◀</button>
|
||||
<button class="meal-nav next" onclick="mealNav(1)" aria-label="Next">▶</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if gt $count 1 }}
|
||||
<div class="meal-dots" id="mealDots">
|
||||
{{ range $i, $r := $items }}
|
||||
<span class="meal-dot{{ if eq $i 0 }} active{{ end }}" data-index="{{ $i }}" onclick="mealGo({{ $i }})"></span>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ if gt $count 1 }}
|
||||
<script>
|
||||
(function() {
|
||||
const scroller = document.getElementById('mealScroller');
|
||||
const dots = document.querySelectorAll('#mealDots .meal-dot');
|
||||
const prevBtn = scroller?.parentElement.querySelector('.meal-nav.prev');
|
||||
const nextBtn = scroller?.parentElement.querySelector('.meal-nav.next');
|
||||
const total = {{ $count }};
|
||||
let current = 0;
|
||||
|
||||
function updateUI() {
|
||||
dots.forEach((d, i) => d.classList.toggle('active', i === current));
|
||||
if (prevBtn) prevBtn.classList.toggle('hidden', current === 0);
|
||||
if (nextBtn) nextBtn.classList.toggle('hidden', current === total - 1);
|
||||
}
|
||||
|
||||
function scrollToIndex(i) {
|
||||
if (!scroller) return;
|
||||
const slide = scroller.children[i];
|
||||
if (slide) {
|
||||
scroller.scrollTo({ left: slide.offsetLeft, behavior: 'smooth' });
|
||||
current = i;
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
window.mealNav = function(dir) {
|
||||
const next = Math.max(0, Math.min(total - 1, current + dir));
|
||||
scrollToIndex(next);
|
||||
};
|
||||
|
||||
window.mealGo = function(i) {
|
||||
scrollToIndex(i);
|
||||
};
|
||||
|
||||
// Track scroll position
|
||||
if (scroller) {
|
||||
scroller.addEventListener('scroll', function() {
|
||||
const scrollLeft = scroller.scrollLeft;
|
||||
const slideWidth = scroller.offsetWidth;
|
||||
const newCurrent = Math.round(scrollLeft / slideWidth);
|
||||
if (newCurrent !== current && newCurrent >= 0 && newCurrent < total) {
|
||||
current = newCurrent;
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateUI();
|
||||
})();
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
- type: bookmarks
|
||||
title: Productivity Self-Hosted
|
||||
groups:
|
||||
|
||||
Reference in New Issue
Block a user