diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-30 08:56:43 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-04-30 08:56:43 +0200 |
| commit | f51250941f4425ce4d63b90a266a29e8588a7d03 (patch) | |
| tree | a6343cc7796462471f48294050da26a99db1be20 /src/pages/blog | |
| parent | 8fecbcbba2eef19f4adf3c95573f8330746335b7 (diff) | |
Add tagging to articles
Diffstat (limited to 'src/pages/blog')
| -rw-r--r-- | src/pages/blog/[id].astro | 61 | ||||
| -rw-r--r-- | src/pages/blog/index.astro | 265 |
2 files changed, 312 insertions, 14 deletions
diff --git a/src/pages/blog/[id].astro b/src/pages/blog/[id].astro index 65c8a25..935a796 100644 --- a/src/pages/blog/[id].astro +++ b/src/pages/blog/[id].astro @@ -61,7 +61,68 @@ const schema = { <time datetime={entry.data.publishedAt.toISOString()}> {formattedDate} </time> + {entry.data.tags && entry.data.tags.length > 0 && ( + <> + • Etiquetas: + <ul class="tags"> + {entry.data.tags.map((tag: string) => ( + <li><a href={`/blog/?tag=${encodeURIComponent(tag)}`}>{tag}</a></li> + ))} + </ul> + </> + )} </small> <Content /> </Layout> + +<style lang="scss"> + @use "../../../styles/variables" as v; + @use "sass:color"; + + .tags { + display: inline-flex; + list-style: none; + margin: 0; + padding: 0; + gap: 0.75rem; + } + + .tags li { + display: inline; + } + + .tags a { + // Estilo de enlace normal, siguiendo los estilos predefinidos en Layout.astro + color: v.$accentDark; + font-size: 0.85rem; + font-family: v.$monoFontStack; + text-decoration: none; + box-shadow: 0 1px v.$accent; + transition: all 0.2s ease; + + &:hover { + box-shadow: 0 2px v.$accentDark; + } + + &:focus { + color: v.$accentDark; + outline: none; + background-color: v.$secondary; + box-shadow: 0 4px #0b0c0c; + } + } + + /* Estilos para la información de la publicación */ + small { + display: block; + margin-top: -1rem; + margin-bottom: 1.5rem; + font-size: 0.85rem; + color: color.adjust(v.$dark, $lightness: 30%); + } + + time { + font-style: italic; + } +</style> diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 68ad02e..b74781b 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -6,6 +6,7 @@ const blogCollection = (await getCollection("blog")).sort((a, b) => { return b.data.publishedAt.getTime() - a.data.publishedAt.getTime(); }); +// Agrupar artículos por fecha const groupedPosts = blogCollection.reduce( (acc: Record<string, any[]>, post) => { const year = post.data.publishedAt.getFullYear(); @@ -20,6 +21,9 @@ const groupedPosts = blogCollection.reduce( {}, ); +// Colección de todas las etiquetas únicas +const allTags = [...new Set(blogCollection.flatMap(post => post.data.tags || []))].sort(); + function humaniseDate(date: Date) { const result = date.toLocaleDateString("es-ES", { month: "long", @@ -64,18 +68,251 @@ const schema = { son mías, y no representan a ninguna empresa o institución. </p> - { - Object.entries(groupedPosts).map(([key, posts]) => ( - <section> - <h2>{humaniseDate(new Date(key))}</h2> - <ul> - {posts.map((post) => ( - <li> - <a href={`/blog/${post.id}`}>{post.data.title}</a> - </li> - ))} - </ul> - </section> - )) - } + {allTags.length > 0 && ( + <div class="tags-container"> + <h2>Etiquetas</h2> + <div class="tag-filter"> + <button class="tag-button active" data-tag="all">Todas</button> + {allTags.map((tag) => ( + <button class="tag-button" data-tag={tag}>{tag}</button> + ))} + </div> + </div> + )} + + <div id="blog-posts"> + { + Object.entries(groupedPosts).map(([key, posts]) => ( + <section class="post-section" data-date={key}> + <h2>{humaniseDate(new Date(key))}</h2> + <ul> + {posts.map((post) => { + const postTags = post.data.tags || []; + const tagsAttribute = postTags.join(','); + return ( + <li class="post-item" data-tags={tagsAttribute}> + <a href={`/blog/${post.id}`}>{post.data.title}</a> + {postTags.length > 0 && ( + <ul class="post-tags"> + {postTags.map((tag: string) => ( + <li> + <button class="tag-link" data-tag={tag}> + {tag} + </button> + </li> + ))} + </ul> + )} + </li> + ); + })} + </ul> + </section> + )) + } + </div> + + <script> + // Script para el filtrado de artículos por etiqueta + document.addEventListener('DOMContentLoaded', () => { + const tagButtons = document.querySelectorAll('.tag-button'); + const tagLinks = document.querySelectorAll('.tag-link'); + const postItems = document.querySelectorAll('.post-item'); + const postSections = document.querySelectorAll('.post-section'); + + // Función para filtrar los artículos por etiqueta + function filterByTag(tag: string) { + // Primero, restablecer la visibilidad de todos los elementos + postItems.forEach(item => { + (item as HTMLElement).style.display = ''; + }); + postSections.forEach(section => { + (section as HTMLElement).style.display = ''; + }); + + // Si no es 'todas', filtrar por etiqueta + if (tag !== 'all') { + postItems.forEach(item => { + const itemEl = item as HTMLElement; + const tagsAttr = itemEl.dataset.tags || ''; + const itemTags = tagsAttr ? tagsAttr.split(',') : []; + if (!itemTags.includes(tag)) { + itemEl.style.display = 'none'; + } + }); + + // Ocultar secciones que no tienen artículos visibles + postSections.forEach(section => { + const items = section.querySelectorAll('.post-item'); + let allHidden = true; + + items.forEach(item => { + if ((item as HTMLElement).style.display !== 'none') { + allHidden = false; + } + }); + + if (allHidden) { + (section as HTMLElement).style.display = 'none'; + } + }); + } + + // Actualizar estado activo de los botones + tagButtons.forEach(button => { + if ((button as HTMLElement).dataset.tag === tag) { + button.classList.add('active'); + } else { + button.classList.remove('active'); + } + }); + + // Actualizar URL con el parámetro de consulta + if (tag === 'all') { + history.replaceState(null, document.title, window.location.pathname); + } else { + history.replaceState(null, document.title, `?tag=${encodeURIComponent(tag)}`); + } + + // Añadir un log para depuración + console.log(`Filtrado por etiqueta: ${tag}`); + console.log(`Artículos visibles: ${document.querySelectorAll('.post-item:not([style*="display: none"])')?.length || 0}`); + } + + // Eventos de clic para los botones de etiquetas + tagButtons.forEach(button => { + button.addEventListener('click', () => { + const tag = (button as HTMLElement).dataset.tag; + if (tag) filterByTag(tag); + }); + }); + + // Eventos de clic para los enlaces de etiquetas dentro de los posts + tagLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const tag = (link as HTMLElement).dataset.tag; + if (tag) filterByTag(tag); + + // Desplazamiento suave hacia arriba para ver todos los resultados + const tagsContainer = document.querySelector('.tags-container'); + if (tagsContainer) { + window.scrollTo({ + top: (tagsContainer as HTMLElement).offsetTop - 20, + behavior: 'smooth' + }); + } + }); + }); + + // Verificar si hay un parámetro de consulta para filtrar + const urlParams = new URLSearchParams(window.location.search); + const tagParam = urlParams.get('tag'); + + if (tagParam) { + const tagExists = Array.from(tagButtons).some(button => + (button as HTMLElement).dataset.tag === tagParam + ); + if (tagExists) { + filterByTag(tagParam); + } + } + }); + </script> </Layout> + +<style lang="scss"> + @use "../../../styles/variables" as v; + @use "sass:color"; + + .tags-container { + margin-bottom: 2rem; + } + + .tag-filter { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 1.5rem; + } + + .tag-button { + padding: 0.25rem 0.6rem; + background-color: v.$light; + color: v.$accent; + border: 1px solid v.$accent; + border-radius: 1.5rem; + font-size: 0.85rem; + font-family: v.$monoFontStack; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: v.$shadow; + + &:hover { + background-color: color.adjust(v.$accent, $lightness: 45%); + color: v.$accentDark; + transform: translateY(-1px); + } + + &.active { + background-color: v.$accent; + color: v.$lightAlt; + border-color: v.$accentDark; + } + } + + .post-tags { + display: inline-flex; + list-style: none; + margin: 0; + padding: 0; + gap: 0.25rem; + margin-left: 0.5rem; + } + + .post-tags li { + display: inline; + } + + .tag-link { + display: inline-block; + padding: 0.1rem 0.4rem; + background-color: color.adjust(v.$background, $lightness: -3%); + color: v.$accentDark; + border: none; + border-radius: 1rem; + font-size: 0.75rem; + font-family: v.$monoFontStack; + text-decoration: none; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: color.adjust(v.$accent, $lightness: 45%); + color: v.$accentDark; + transform: translateY(-1px); + } + } + + /* Efecto de transición para el filtrado */ + .post-item { + transition: all 0.3s ease; + } + + .post-section { + transition: opacity 0.3s ease; + } + + #blog-posts { + min-height: 200px; + } + + /* Mejora el aspecto de los enlaces de posts */ + .post-item { + margin-bottom: 0.5rem; + + a { + font-weight: 500; + } + } +</style> |
