This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Monday, 10 November 2025
Κατά τη διάρκεια των τελευταίων ημερών έχω σφυροκοπήσει μακριά στο νέο blog filter bar: επιλογή γλώσσας, παραγγελία, και ένα range picker που προσπαθεί πολύ σκληρά να είναι έξυπνος (μερικές φορές πάρα πολύ έξυπνος).
Αυτή η ανάρτηση περπατάει μέσα από ό, τι έφτιαξα, γιατί μερικά πράγματα πήγαν στραβά (ραντεβού δεν συμπεριφέρονται, τονίζει δεν αναζωογονητικό), και πώς τα έφτιαξα.
Θυμηθείτε αυτό το site είναι ένα έργο σε εξέλιξη, αυτό το είδος των πραγμάτων θα συμβεί όταν θα φάτε-σας-own-dogfood!
/blog/date-rangeΤελικό όριο εύρους ημερομηνίας<clear-param>Σαφής βοηθός ετικέτας param: Εύκολος τρόπος για να καθαρίσετε τα φίλτρα χρησιμοποιώντας
Κατά μήκος του τρόπου που είχα μερικά σφάλματα: εύρος ημερομηνίας που χάνονται στην αλλαγή γλώσσας / παραγγελίας, το ημερολόγιο τονίζει δεν αναζωογονητικό σε swap, και ένα διασκεδαστικό ένα όπου σκοτεινό στυλ λειτουργίας έκανε το ημερολόγιο να φαίνεται σπασμένο, επειδή Flatpickr δεν επανασχεδιάστηκε.
<div id="filters" hx-target="#content">
<select id="languageSelect">…</select>
<select id="orderSelect">
<option value="date_desc">Newest first</option>
<option value="date_asc">Oldest first</option>
<option value="title_asc">Title A–Z</option>
<option value="title_desc">Title Z–A</option>
</select>
<input id="dateRange" type="text" placeholder="YYYY-MM-DD → YYYY-MM-DD" />
<button id="clearDateFilter">Clear</button>
<div id="filterSummary"></div>
</div>
<div id="content"><!-- HTMX swaps blog list here --></div>
Αυτή είναι η σκληρή δομή που περιμένει το σενάριο:Mostlylucid/src/js/blog-index.jsΠελάτης: HTMX + Flatpickr καλωδίωση
function applyNavigation(u){
const target = document.querySelector('#content');
try{ window.history.pushState({}, '', u.toString()); }catch{}
if(window.htmx && target){
window.htmx.ajax('GET', u.toString(), {
target: '#content',
swap: 'outerHTML show:none',
headers: {'pagerequest': 'true'}
});
} else {
window.location.href = u.toString();
}
}
const url = new URL(window.location.href);
const existingStart = url.searchParams.get('startDate');
const existingEnd = url.searchParams.get('endDate');
const existingLang = url.searchParams.get('language') || 'en';
const existingOrderBy = (url.searchParams.get('orderBy') || 'date').toLowerCase();
const existingOrderDir = (url.searchParams.get('orderDir') || 'desc').toLowerCase();
langSelect.value = existingLang;
orderSelect.value = `${existingOrderBy}_${existingOrderDir}`;
updateSummary();
async function fetchMonth(year, month, language){
const res = await fetch(`/blog/calendar-days?year=${year}&month=${month}&language=${encodeURIComponent(language||'en')}`);
if(!res.ok) return new Set();
const j = await res.json();
return new Set(j.dates || []);
}
function formatYMD(d){ return d.toISOString().substring(0,10); }
let highlightDates = new Set();
const fp = window.flatpickr(input, {
mode: 'range',
dateFormat: 'Y-m-d',
defaultDate: [existingStart, existingEnd].filter(Boolean),
onDayCreate: function(_dObj,_dStr,fpInstance,dayElem){
const ymd = formatYMD(dayElem.dateObj);
if(highlightDates.has(ymd)){
dayElem.classList.add('has-post');
dayElem.style.background = 'rgba(76,175,80,0.35)';
dayElem.style.borderRadius = '6px';
}
},
onMonthChange: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onOpen: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onChange: function(selectedDates){
if(selectedDates.length === 2){
const [start,end] = selectedDates;
const u = new URL(window.location.href);
u.searchParams.set('startDate', formatYMD(start));
u.searchParams.set('endDate', formatYMD(end));
u.searchParams.set('page','1');
u.searchParams.set('language', langSelect.value);
const [ob,od] = orderSelect.value.split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
updateSummary();
applyNavigation(u);
}
}
});
langSelect.addEventListener('change', async function(){
const u = new URL(window.location.href);
u.searchParams.set('language', langSelect.value);
u.searchParams.set('page', '1');
const [ob,od] = (orderSelect.value||'date_desc').split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
if(input._flatpickr && input._flatpickr.selectedDates.length===2){
const [s,e] = input._flatpickr.selectedDates;
u.searchParams.set('startDate', formatYMD(s));
u.searchParams.set('endDate', formatYMD(e));
}
// refresh calendar highlights for new language
if(input._flatpickr){
const fp = input._flatpickr;
highlightDates = await fetchMonth(fp.currentYear, fp.currentMonth+1, langSelect.value);
fp.redraw();
}
updateSummary();
applyNavigation(u);
});
const obs = new MutationObserver(() => {
const dr = root.querySelector('#dateRange');
const fp = dr && dr._flatpickr;
if(fp && typeof fp.redraw === 'function') fp.redraw();
});
obs.observe(document.documentElement, { attributes:true, attributeFilter:['class'] });
Διατηρήστε τις ημερομηνίες κατά την αλλαγή γλώσσας ή παραγγελίας
calendar-daysΤρία τελικά σημεία τροφοδοτούν τη σελίδα:date-rangeΑ// GET /blog
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
VaryByQueryKeys = new[] { "page", "pageSize", nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) },
Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
VaryByQueryKeys = new[] { nameof(page), nameof(pageSize), nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) })]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20, DateTime? startDate = null, DateTime? endDate = null,
string language = MarkdownBaseService.EnglishLanguage, string orderBy = "date", string orderDir = "desc")
{
var posts = await blogViewService.GetPagedPosts(page, pageSize, language: language, startDate: startDate, endDate: endDate);
posts.LinkUrl = Url.Action("Index", "Blog", new { startDate, endDate, language, orderBy, orderDir });
if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
return View("Index", posts);
}
// GET /blog/calendar-days?year=2025&month=11&language=en
[HttpGet("calendar-days")]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 1800, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) })]
public async Task<IActionResult> CalendarDays(int year, int month, string language = MarkdownBaseService.EnglishLanguage)
{
if (year < 2000 || month < 1 || month > 12) return BadRequest("Invalid year or month");
var start = new DateTime(year, month, 1);
var end = start.AddMonths(1).AddDays(-1);
var posts = await blogViewService.GetPostsForRange(start, end, language: language);
if (posts is null) return Json("");
var dates = posts.Select(p => p.PublishedDate.Date).Distinct().OrderBy(d => d).Select(d => d.ToString("yyyy-MM-dd")).ToList();
return Json(new { dates });
}
// GET /blog/date-range?language=en
[HttpGet("date-range")]
[ResponseCache(Duration = 3600, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 7200, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(language) })]
public async Task<IActionResult> DateRange(string language = MarkdownBaseService.EnglishLanguage)
{
var allPosts = await blogViewService.GetAllPosts();
if (allPosts is null || !allPosts.Any())
{
return Json(new
{
minDate = DateTime.UtcNow.AddYears(-1).ToString("yyyy-MM-dd"),
maxDate = DateTime.UtcNow.ToString("yyyy-MM-dd")
});
}
var posts = allPosts;
if (!string.IsNullOrEmpty(language) && language != MarkdownBaseService.EnglishLanguage)
{
posts = allPosts.Where(p => p.Language.Equals(language, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (!posts.Any()) posts = allPosts;
var minDate = posts.Min(p => p.PublishedDate.Date);
var maxDate = posts.Max(p => p.PublishedDate.Date);
return Json(new
{
minDate = minDate.ToString("yyyy-MM-dd"),
maxDate = maxDate.ToString("yyyy-MM-dd")
});
}
τελικό σημείο που επιστρέφει το σύνολο των ημερών σε ένα δεδομένο μήνα που έχουν θέσεις (για στιγμές)LinkUrlΑ
public class BasePagingModel<T> : Interfaces.IPagingModel<T> where T : class
{
public int Page { get; set; }
public int TotalItems { get; set; } = 0;
public int PageSize { get; set; }
public ViewType ViewType { get; set; } = ViewType.TailwindAndDaisy;
public string LinkUrl { get; set; }
public List<T> Data { get; set; }
}
sequenceDiagram
participant U as User
participant FP as Flatpickr
participant JS as blog-index.js
participant HT as HTMX
participant C as BlogController
U->>FP: Selects 2025-11-01 → 2025-11-10
FP-->>JS: onChange([start,end])
JS->>JS: Update URLSearchParams (startDate, endDate, language, order)
JS->>HT: htmx.ajax('GET', /blog?...)
HT->>C: GET /blog with query
C-->>HT: Partial _BlogSummaryList
HT-->>U: Swap #content
JS->>FP: (after swap) ensure highlights + redraw
flowchart TD A[Open calendar / Month change] --> B[Fetch /blog/calendar-days] B -->|JSON dates: yyyy-mm-dd array| C[Set highlightDates] C --> D[flatpickr redraw] D --> E[onDayCreate adds .has-post]
Ροή: ανταύγειες ημερολογίουBlogPostEntity:
public class BlogPostEntity
{
// ... existing fields ...
public bool IsPinned { get; set; }
public bool IsHidden { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
}
Νέα Χαρακτηριστικά: Ορατότητα και Έλεγχος ΠεριεχομένουBlogServiceΦίλτρα μετά την ορατότητα
// Filter out hidden posts and posts scheduled for the future
var now = DateTimeOffset.UtcNow;
postQuery = postQuery.Where(x =>
!x.IsHidden &&
(x.ScheduledPublishDate == null || x.ScheduledPublishDate <= now));
// For page 1, prioritize pinned posts
var isFirstPage = page == null || page.Value == 1;
if (isFirstPage)
{
postQuery = postQuery.OrderByDescending(x => x.IsPinned)
.ThenByDescending(x => x.PublishedDate.DateTime);
}
Η μεγαλύτερη βελτίωση σε αυτή την ενημέρωση είναι η σωστή διαχείριση μετά την προβολή.
IsHidden = trueΗScheduledPublishDateIsPinned = true) never appear in listsΕμφανίζονται μόνο μετά από
[HtmlTargetElement("clear-param")]
public class ClearParamTagHelper : TagHelper
{
[HtmlAttributeName("name")]
public string? Name { get; set; }
[HtmlAttributeName("all")]
public bool All { get; set; } = false;
[HtmlAttributeName("exclude")]
public string Exclude { get; set; } = "";
// ... styling and Alpine.js integration ...
}
Καρφιτσωμένες θέσεις
<!-- Clear a specific parameter -->
<clear-param name="startDate">Clear Date</clear-param>
<!-- Clear all parameters except language -->
<clear-param all="true" exclude="language">Clear All Filters</clear-param>
) πάντα εμφανίζονται πρώτη στη σελίδα 1, ιδανικό για ανακοινώσεις ή χαρακτήρισε περιεχόμενοwindow.queryParamClearerΚαθαρίστε τον βοηθό ετικετών Param
**Ένας νέος βοηθός ετικέτας ASP.NET Core καθιστά εύκολο να καθαρίσει παραμέτρους ερώτημα:**Χρήση σε θέα Razor:
συστατικό για τη διαχείριση των πράξεων χειραγώγησης URL και ανταλλαγής HTMX.
startDate/endDate.new URL(window.location.href)Θα καλύψω τα πλήρη στοιχεία εφαρμογής, συμπεριλαμβανομένων των συστατικών Alpine.js και tag helper μοτίβα, σε μια μελλοντική δημοσίευση για την κατασκευή επαναχρησιμοποίητων συστατικών HTMX / Alpine.language/orderΖουζούνια που χτύπησα (και οι διορθώσεις)Χαμένο εύρος ημερομηνίας για αλλαγή γλώσσας/παραγγελίας
και να αλλάξει μόνο τα μέρη που άλλαξαν; ή να διαβάσετε ημερομηνίες από Flatpickr αν υπάρχουν.
<html class>Οι στιγμές του ημερολογίου δεν αναζωογονήθηκαν μετά από ανταλλαγή HTMXfp.redraw().Αιτία: Flatpickr περίπτωση ζούσε στο παλιό DOM και πέθανε σε ανταλλαγή.
LinkUrlΕπίσης re-hook μετά τις πράξεις ανταλλαγής (βλέπε παρακάτω).posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })Αιτία: οι τάξεις στυλ άλλαξαν αλλά Flatpickr dint redraw ζωγραφισμένες ημέρες.αλλαγές και κλήση
(function(){
function initFromRoot(root){ /* …the big function shown above… */ }
if(window.htmx){
document.body.addEventListener('htmx:afterSwap', function(ev){
const tgt = ev.detail.target;
// re-init if the content swapped includes the filters/content area
if(tgt && (tgt.id === 'content' || tgt.querySelector?.('#filters'))){
initFromRoot(document);
}
});
}
// also run on first load
initFromRoot(document);
})();
Έτσι ο βομβητής συνθέτει σωστά URL.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.