# ProgrÃ ̈s de la barre de filtration (et les Shenanigans de rÃ©partition des datesM SK1

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

## Introduction

Au cours des derniers jours, j’ai hammering away à la nouvelle barre de filtrage du blog: sélection linguistique, commandesM SK3 et un picker d’éventail de dates qui essaie très dur d’être intelligente ( parfois trop intelligente~). Ce post passe par ce que je construiseMSC6 pourquoi quelques choses ont été détournées MST7 dates ne se comportent pas~MST8 mises en évidence non rafraîchissantes~M ST9 et comment je les ai corrigées~M st10 Plenty of source code and a couple of mermaid diagrams to show the flowM ST11

> rappelez-vous que ce site est un travail en cours.

[TOC]

## Ce qui a changé?

J’ai ajouté une barre de filtrage appropriée à l’indice du blog qui comprend :

- Choisissez une langue qui préserve vos filtres actuels et re-request la liste via HTMX
- Ordre de tri sélectionner (dateM SK1title asc/desc ) qui préserve également le contexte
- Un picker de portée de date (Flatpickr en mode de portée) avec serveurM SK2éclairages de jour dirigés
- L'état URL est toujours en sync (retour/travail vers l'avantM SK2traveillement des liens profonds )
- Le caching du serveur varie maintenant correctement selon les paramètres de filtrage

### Nouvelles dans cette mise à jour (The Big Fix!)

- **Filtres de visibilité**: Les postes peuvent maintenant être cachés , prévus pour une publication future M SK2 ou attachés au haut
- **Point final de l'éventail des dates**: Nouveau `/blog/date-range` L'API retourne des dates min/max pour les limites sensibles du capteur
- **Supprimer l'aide à la balise param**: Méthode facile pour nettoyer les filtres en utilisant `<clear-param>` Aide-étiquette avec Alpine.js

Par la suite, j’ai eu quelques bugs: les plages de dates sont perdues sur le langage / changement d’ordre , les mises en vedette du calendrier ne rafraîchissant pas sur l’échange M SK3 et un fun où les styles de mode foncé ont fait que le calendrier apparaissait défaillant parce Flatpickr n’était pas

## La barre de filtrage dans le DOM

C’est la structure brute que le script attend:

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

## Client: HTMX + Établissement d'un câblage flatpickr

Le noyau du comportement vit dans `Mostlylucid/src/js/blog-index.js`. Quelques pièces importantesM SK1

- Conservation des URL et navigation 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();
  }
}
```

- Initialisation, par défaut depuis URL, et maintien de l'UI sommaire en sync

```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 range with server‑driven day highlights

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

- Préserver les dates au moment de changer la langue ou l'ordre

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

- Re-draw calendar when the theme toggles (darkM SK2light) 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'] });
```

## Server: finpoints et caching

Trois points finals alimentent la page:

- L'index lui-même, qui accepte la pageM SK1 pageSize, startDateMSC3endDate , languageMST5 et ordre*
- A `calendar-days` point final qui retourne la série de jours d'un mois donné ayant des postes (pour les points saillantsM SK1
- A `date-range` point final qui retourne les dates minimales/max dans tous les postes M SK1language-aware )

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

Un avertissement rapide: le modèle de messagerie a `LinkUrl` de sorte que la pagination préserve le contexte actuel du filtre lorsque le serveur est rendu.

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

## Séquence: comment un changement de filtre s'écoule

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

## Faits saillants du calendrier Flow:

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

## Nouvelles fonctionnalités: Visibilité et contrôle du contenu

### Post Visibility Filters

La plus grande amélioration de cette mise à jour est la bonne gestion de la visibilité postérieure. Trois nouveaux champs ont été ajoutés : `BlogPostEntity`:

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

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

L'article 5 de la Loi sur l'immigration et le statut des réfugiés `BlogService` maintenant filtre les postes de façon appropriée:

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

Cela signifie:

- **Postes cachés** (`IsHidden = true`) n’apparaît jamais dans les listes
- **Postes prévus** apparaissent seulement après leur `ScheduledPublishDate`
- **Postes affilés** (`IsPinned = true`) apparaît toujours au premier rang de la page 1, parfait pour les annonces ou le contenu en vedette

### Supprimer l'aide à la balise Param

Un nouvel outil d'aide de balises ASP.NET Core facilite la clarté des paramètres de recherche

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

Usage dans les vues de Razor:

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

L'aide-étiquette est intégrée avec Alpine.jsM SK1s `window.queryParamClearer` composant pour gérer la manipulation des URL et les échanges HTMX.

> **Remarque**: Tant l’aideur de balises de messagerie que le paramètre clarifier des demandes méritent une exploration plus approfondie . IM SK2 Je vais aborder les détails de la mise en œuvre intégrale, incluant la composante AlpineMSC4js et les modèles d’aideurs de baliseMST5 dans un futur article sur la construction de HTMX réutilisablesMSP6Components alpinsMSS7

## Les erreurs que j’ai rencontrées (et les corrections)

- Dates perdues lors du changement d'ordre de la langue/
  
  - Cause: J’ai construit la nouvelle URL à partir de zéro, pas de celle actuelleM SK2 perdue `startDate`/`endDate`.
  - Fix: Commencez toujours avec `new URL(window.location.href)` et ne changer que les parties qui ont changé; ou lire des dates de Flatpickr si elles sont présentes `language`/`order` Changer les gestionnaires ci-dessus.

- Les mises en valeur du calendrier n’ont pas été renouvelées’ après un échange HTMX
  
  - Cause: L'instance Flatpickr vivait sur le vieux DOM et décède sur swap.
  - Fix: À l’init, détruire toute instance existante , reMSC3 créerM SK4 puis précharger les mises en valeur pour le mois visibleMNK5 ReMEK6 arrêter après les échanges MRK7voir ci-dessousMMK8

- Le mode foncé a fait que le calendrier apparaisse bloqué
  
  - Cause: les classes de style ont changé mais Flatpickr n’a pas rétabli les jours peints
  - Fix: Observe `<html class>` Changements et appels `fp.redraw()`.

- Liens de pagination renvoyés dans le contexte du filtre
  
  - Cause: `LinkUrl` n’était pas établie pour inclure la demande dans certains cas’
  - Fix: Sur le serveur `posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })` de sorte que le navigateur compose correctement les URL.

## HTMX swap safety: re-init sur demande

Si vous utilisez des swaps partiels, votre JS doit être 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);
})();
```

## Qu’est-ce qui se passera prochainement?

- Ajouter des prévisions rapides (La dernière 7 jours MSC2 MSC3 jours
- Langue persistante dans localStorage pour semer la page
- Accords de clavier pour la gamme des dates

Si vous remarquez quelque chose de étrange avec les filtres, veuillez laisser un commentaire dans votre navigateur M SK1 étapes . Merci pour le dogfooding avec moi!