# फ़िल्टर पट्टी प्रगति

<!--category-- ASP.NET, HTMX, JavaScript -->
<datetime class="hidden">2025-11-10T09:00</datetime>

## परिचय

पिछले कुछ दिनों में मैं-'-मैं नए ब्लॉग फिल्टर पट्टी पर hammering away कर रहा हूँ।

> याद रखें यह साइट एक काम प्रगति में है।

[TOC]

## क्या बदला है

यहाँ>'> उच्च है।

- भाषा चुनें जो आपके वर्तमान फ़िल्टरों को सुरक्षित रखती है और HTMX के माध्यम से सूची की मांग करती है
- क्रमबद्ध क्रम चुनें (dateM SK1title asc/desc ) जो संदर्भ को भी सुरक्षित करता है
- एक तिथि रेंज पिकर (Flatpickr रेंज मोड में) सर्वर के साथM SK2प्रचालित दिन हाइलाइट्स
- यूआरएल स्थिति हमेशा समक्रमण में रखी जाती है (back/forward worksMSC2 deep links workM SK3
- सर्वर कैचिंग अब फ़िल्टर पैरामीटरों द्वारा सही रूप से बदलता है

### इस अद्यतन में नया (द बिग फिक्स

- **दृश्यता फिल्टर**: डाक अब छुपा जा सकता है
- **तिथि रेंज अंतबिन्दु**: नया `/blog/date-range` API sensible picker bounds के लिए min/max तिथियां लौटाता है
- **पारम टैग मददगार साफ करें**: फ़िल्टरों को साफ करने का आसान तरीका उपयोग कर `<clear-param>` के साथ टैग मददगार Alpine.js

रास्ते में मेरे पास कुछ बगें थीं-: भाषा पर तिथि रेंजों को हरा जा रहा था।

## डोम में फ़िल्टर पट्टी

यह स्क्रिप्ट की प्रत्याशित rough संरचना है

```html
<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>
```

## क्लाइंट: HTMX + फ्लैट पिक्सर तार

व्यवहार के मूल में निवास करता है `Mostlylucid/src/js/blog-index.js`कुछ महत्वपूर्ण टुकड़े

- यूआरएल संरक्षण और HTMX नेविगेशन

```js
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();
  }
}
```

- आरंभीकरण, यूआरएल से डिफ़ॉल्ट हो रहा है, और सारांश UI को समक्रमित बनाए रखना

```js
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();
```

- सर्वर के साथ Flatpickr दायरा

```js
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);
    }
  }
});
```

- भाषा या आदेश बदलने पर तारीखें सुरक्षित रखें

```js
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);
});
```

- जब विषयवस्तु टॉगल करता है तो पुनः -draw कैलेन्डर | ( | dark | / | light | ) so Flatpickr repaints

```js
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` अंतबिंदु जो सभी पदों पर न्यूनतम /max तारीख को लौटाता है

```csharp
// 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` इस तरह कि पृष्ठीकरण वर्तमान फिल्टर संदर्भ को सुरक्षित करता है जब rendered सर्वर

```csharp
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; }
}
```

## अनुक्रम: कैसे एक फिल्टर परिवर्तन प्रवाहित होता है

```mermaid
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
```

## फ्लो

```mermaid
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`:

```csharp
public class BlogPostEntity
{
    // ... existing fields ...

    public bool IsPinned { get; set; }
    public bool IsHidden { get; set; }
    public DateTimeOffset? ScheduledPublishDate { get; set; }
}
```

 `BlogService` अब पोस्ट उचित रूप से फिल्टर करता है

```csharp
// 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`) सूची में कभी नहीं दिखाई देता
- **अनुसूचित पदों** केवल उनके बाद प्रकट `ScheduledPublishDate`
- **पिन्ड पोस** (`IsPinned = true`) हमेशा पृष्ठ पर पहली बार दिखाई देता है 1, घोषणाओं या विशिष्ट सामग्री के लिए उपयुक्त

### पैराम टैग मददगार साफ करें

एक नया ASP.NET कोर टैग मददगार क्वेरी पैरामीटरों को साफ करने के लिए आसान करता है

```csharp
[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 ...
}
```

रजोर दृश्यों में उपयोग

```cshtml
<!-- 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>
```

टैग सहायक Alpine के साथ एकीकृत करता है `window.queryParamClearer` युआरएल परिचालन और HTMX स्वाप को संभालने के लिए घटक.

> **नोट**Both the paging tag helper and the query parameter clearer deserve deeper exploration

## बग्स मैं Hit (and the fixes)

- भाषा के बदलने पर तिथि दायरा हरा गया
  
  - कारण: मैं नए यूआरएल को शून्य से बना दिया था `startDate`/`endDate`.
  - Fix: हमेशा के साथ शुरू करें `new URL(window.location.href)` और केवल उन भागों को बदलें जो बदल गए हैं; या Flatpickr से तिथियां पढ़ने के लिए अगर मौजूद है `language`/`order` उपर्युक्त हैंडलर बदलें

- पंचांग हाइलाइट HTMX स्क्वाप के बाद ताज़ा नहीं हुआ
  
  - कारण
  - ठीक करें: इनिट पर, किसी भी मौजूदा इंस्टैंस को नष्ट कर दें , फिर पुनः‑नृजित करें M SK4 फिर दृश्यात्मक माह के लिए पूर्वलोड हाइलाइट्स लाएँ।

- अंधेरा मोड ने कैलेंडर को टूटा दिखाई
  
  - कारण: शैली वर्गों में परिवर्तन हुआ लेकिन फ्लैटपिकर ने नहीं किया
  - स्थिर `<html class>` परिवर्तन और कॉल `fp.redraw()`.

- पृष्ठीकरण लिंक फ़िल्टर संदर्भ छोड़ दिया
  
  - कारण `LinkUrl` कुछ मामलों में क्वेरी शामिल करने के लिए सेट नहीं था
  - सर्वर पर `posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })` तो पेजर सही रूप से यूआरएल बनाता है

## HTMX स्क्वाप सुरक्षा

अगर आप आंशिक स्वैप का उपयोग करते हैं, तो आपके JS को idempotent होना चाहिए.

```js
(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);
})();
```

## आगे क्या है

- त्वरित पूर्वनिर्धारित जोड़ें (अंतिम | 7 | दिन |/ | МSK3
- पृष्ठ को बीजित करने के लिए स्थानीय में स्थायी भाषा
- तिथि शृंखला के लिए कुंजीपटल आवंटन

यदि आप फ़िल्टरों के साथ कोई अजीब बात पाते हैं तो कृपया अपने ब्राउज़र से एक टिप्पणी छोड़ें