diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-05 20:03:27 +0200 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-06-05 20:03:27 +0200 |
| commit | a2830a0dd6f634147456406c7855881ff298078e (patch) | |
| tree | 93af1b60258b0b19a739b294fa31f201c2d64158 /src/layouts | |
| parent | a423c9b15bdf43d28390fb0424dfeec012d82828 (diff) | |
Refresh portfolio design and fonts
Diffstat (limited to 'src/layouts')
| -rw-r--r-- | src/layouts/BlogListLayout.astro | 325 | ||||
| -rw-r--r-- | src/layouts/Layout.astro | 5 | ||||
| -rw-r--r-- | src/layouts/PortfolioItemLayout.astro | 77 | ||||
| -rw-r--r-- | src/layouts/PortfolioPageLayout.astro | 110 |
4 files changed, 452 insertions, 65 deletions
diff --git a/src/layouts/BlogListLayout.astro b/src/layouts/BlogListLayout.astro new file mode 100644 index 0000000..4ad72ea --- /dev/null +++ b/src/layouts/BlogListLayout.astro @@ -0,0 +1,325 @@ +--- +import { getCollection } from "astro:content"; +import Layout from "@/layouts/Layout.astro"; + +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(); + const month = post.data.publishedAt.getMonth() + 1; + const key = `${year}-${month}`; + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(post); + return acc; + }, + {}, +); + +// 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", + year: "numeric", + }); + return result.charAt(0).toUpperCase() + result.slice(1); +} + +const schema = { + "@context": "https://schema.org", + "@type": "Blog", + headline: "Blog de Ariel Costas", + description: + "En este blog encontrarás artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución.", + publisher: { + "@type": "Person", + name: "Ariel Costas Guerrero", + url: "https://www.costas.dev", + }, + author: { + "@type": "Person", + name: "Ariel Costas Guerrero", + url: "https://www.costas.dev", + }, +}; +--- + +<Layout + title="Blog" + description="Artículos sobre desarrollo, tecnología y otras temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones son mías, y no representan a ninguna empresa o institución." +> + <script + is:inline + type="application/ld+json" + slot="head-jsonld" + set:html={JSON.stringify(schema)} + /> + + <h1>Blog de Ariel Costas</h1> + + <p> + En este blog encontrarás artículos sobre desarrollo, tecnología y otras + temáticas que pueda querer compartir. Disclaimer de siempre: las opiniones + son mías, y no representan a ninguna empresa o institución. + </p> + + {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> + 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'); + + function filterByTag(tag: string) { + postItems.forEach(item => { + (item as HTMLElement).style.display = ''; + }); + postSections.forEach(section => { + (section as HTMLElement).style.display = ''; + }); + + 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'; + } + }); + + 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'; + } + }); + } + + tagButtons.forEach(button => { + if ((button as HTMLElement).dataset.tag === tag) { + button.classList.add('active'); + } else { + button.classList.remove('active'); + } + }); + + if (tag === 'all') { + history.replaceState(null, document.title, window.location.pathname); + } else { + history.replaceState(null, document.title, `?tag=${encodeURIComponent(tag)}`); + } + } + + tagButtons.forEach(button => { + button.addEventListener('click', () => { + const tag = (button as HTMLElement).dataset.tag; + const isCurrentlyActive = button.classList.contains('active'); + + if (tag && isCurrentlyActive && tag !== 'all') { + // If the clicked tag is already active, switch back to "all" + filterByTag('all'); + } else if (tag) { + // Otherwise apply the tag filter + filterByTag(tag); + } + }); + }); + + tagLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const tag = (link as HTMLElement).dataset.tag; + + // Check if this tag is already active + const isCurrentlyActive = Array.from(tagButtons).some(button => + (button as HTMLElement).dataset.tag === tag && button.classList.contains('active') + ); + + if (tag && isCurrentlyActive) { + // If the clicked tag is already active, switch back to "all" + filterByTag('all'); + } else if (tag) { + // Otherwise apply the tag filter + filterByTag(tag); + } + + const tagsContainer = document.querySelector('.tags-container'); + if (tagsContainer) { + window.scrollTo({ + top: (tagsContainer as HTMLElement).offsetTop - 20, + }); + } + }); + }); + + // 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> diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 976e8a3..0df9191 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,6 +1,5 @@ --- -import "@fontsource/beiruti"; -import "@fontsource-variable/sen"; +import "@fontsource-variable/onest"; import Header from "../partials/Header.astro"; import Footer from "../partials/Footer.astro"; @@ -111,7 +110,7 @@ const { title, description, empty } = Astro.props; align-self: center; - max-width: 82ch; + max-width: min(1000px, 90vw); font-size: 1.2rem; padding-block-end: 3rem; diff --git a/src/layouts/PortfolioItemLayout.astro b/src/layouts/PortfolioItemLayout.astro new file mode 100644 index 0000000..95715cb --- /dev/null +++ b/src/layouts/PortfolioItemLayout.astro @@ -0,0 +1,77 @@ +--- +import Layout from "@/layouts/Layout.astro"; +import { render } from "astro:content"; +import TechnologyBadge from "@/components/TechnologyBadge.astro"; +import type { InferEntrySchema } from "astro:content"; +import { Icon } from "astro-icon/components"; + +interface Props { + entry: any; +} + +const { entry } = Astro.props; +const data = entry.data as InferEntrySchema<"portfolio">; +const { Content } = await render(entry); +--- + +<Layout title={data.title} description={data.description}> + <a id="link-back" href="/portfolio"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + class="w-6 h-6 inline-block mr-2" + > + <polyline points="15 18 9 12 15 6"></polyline> + </svg> + Volver al portfolio + </a> + + <h1>{data.title}</h1> + + <Content /> + + <div> + {data.githubLink && <a href={data.githubLink}> + <Icon name="tabler:brand-github"/> + GitHub + </a>} + + {data.onlineLink && <a href={data.onlineLink} target="_blank" rel="noopener noreferrer"> + <Icon name="tabler:link"/> + En línea + </a>} + + {data.demoLink && <a href={data.demoLink} target="_blank" rel="noopener noreferrer"> + <Icon name="tabler:link"/> + Demo + </a>} + </div> + + <h2>Tecnologías utilizadas</h2> + + { + data.technologies.map((technology: string) => ( + <TechnologyBadge size="small" code={technology} /> + )) + } +</Layout> + +<style> + a#link-back { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + text-transform: uppercase; + transition: color 0.2s ease-in-out; + } + + a#link-back svg { + height: 1em; + } +</style> diff --git a/src/layouts/PortfolioPageLayout.astro b/src/layouts/PortfolioPageLayout.astro index c2d2b38..e213c10 100644 --- a/src/layouts/PortfolioPageLayout.astro +++ b/src/layouts/PortfolioPageLayout.astro @@ -2,7 +2,7 @@ import t from "../i18n/es.json"; import Layout from "./Layout.astro"; -import TechnologyBadge from "../partials/TechnologyBadge.astro"; +import PortfolioProject from "@/components/PortfolioProject.astro"; const schema = { "@context": "https://schema.org", @@ -29,31 +29,33 @@ const schema = { <p>{t.portfolioPage.freelanceDesc}</p> <section> - <article> - <h3>{t.portfolioPage.orderExtractorTitle}</h3> + <PortfolioProject + title="Extractor de pedidos venta online" + summary="Aplicación de escritorio que extrae los datos sobre los pedidos on-line de diversas plataformas (como WooCommerce, Amazon y Ebay)." + tags={["java", "windows"]} + detailsLink="/portfolio/order-extractor" + /> - <p set:html={t.portfolioPage.orderExtractorDesc} /> + <PortfolioProject + title="Museo a ceo aberto de Ponteareas" + summary="Desarrollo de visualización de contenido turístico para el Concello de Ponteareas. Incluye contenido en 360º e incrustación de vídeos de YouTube. Realizado en 2021." + tags={["php", "web"]} + detailsLink="/portfolio/qr-ponteareas" + /> - <TechnologyBadge code="java" /> - <TechnologyBadge code="windows" /> - </article> + <PortfolioProject + title="QR Touro turístico" + summary="Desarrollo de un generador estático en TypeScript para información turística del Concello de Touro. Realizado en 2020." + tags={["typescript", "web"]} + detailsLink="/portfolio/qr-touro" + /> - <article> - <h3>{t.portfolioPage.touristInfoTitle}</h3> - - <p set:html={t.portfolioPage.touristInfoDesc} /> - - <TechnologyBadge code="php" /> - <TechnologyBadge code="mysql" /> - </article> - - <article> - <h3>{t.portfolioPage.wpConsultingTitle}</h3> - - <p set:html={t.portfolioPage.wpConsultingDesc} /> - - <TechnologyBadge code="php" /> - </article> + <PortfolioProject + title="Consultoría WordPress" + summary="Trabajos de mantenimiento, optimización y migración de sitios web WordPress y tiendas online WooCommerce." + tags={["php", "wordpress"]} + detailsLink="/portfolio/wp-consulting" + /> </section> <h2>{t.portfolioPage.ownProjectsTitle}</h2> @@ -61,52 +63,36 @@ const schema = { <p>{t.portfolioPage.ownProjectsDesc}</p> <section> - <article> - <h3>{t.portfolioPage.personalWebTitle}</h3> - - <p>{t.portfolioPage.personalWebDesc}</p> - - <TechnologyBadge code="astro" /> - <TechnologyBadge code="azure" /> - </article> - - <article> - <h3>{t.portfolioPage.mientrenoTitle}</h3> + <PortfolioProject + title="Web personal" + summary="Desarrollado con Astro, un generador de sitios web estáticos que permite escribir contenido en Markdown y publicar en la web con un rendimiento excelente. Desplegado via GitHub Actions en mi servidor." + tags={["astro", "github", "ubuntu"]} + githubLink="https://github.com/arielcostas/costasdev" + /> - <p set:html={t.portfolioPage.mientrenoDesc} /> + <PortfolioProject + title="MiEntreno (proyecto fin de ciclo)" + summary="Aplicación web para la gestión de entrenamientos deportivos, con una interfaz sencilla y fácil de usar. Desarrollado con ASP.NET Core, Razor Pages y SQL Server." + tags={["dotnet", "azure"]} + githubLink="https://github.com/arielcostas/mientreno" + detailsLink="/portfolio/mientreno" + /> - <TechnologyBadge code="dotnet" /> - <TechnologyBadge code="sqlserver" /> - <TechnologyBadge code="azure" /> - <TechnologyBadge code="rabbitmq" /> - </article> - - <article> - <h3>{t.portfolioPage.vigo360Title}</h3> - - <p set:html={t.portfolioPage.vigo360Desc} /> - - <TechnologyBadge code="go" /> - <TechnologyBadge code="mysql" /> - <TechnologyBadge code="linux" /> - </article> + <PortfolioProject + title="Vigo 360" + summary="Blog sobre Vigo y su entorno, orientado principalmente a hablar de movilidad y toponimia. Desarrollado en Go, con base de datos MySQL y desplegado sobre VPS administrado por mí mismo." + tags={["go", "mysql", "ubuntu"]} + githubLink="https://github.com/arielcostas/vigo360" + detailsLink="/portfolio/vigo-360" + onlineLink="https://vigo360.es" + /> </section> </Layout> <style> section { display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); - gap: 1rem; - } - - article { - padding: 1rem; - border: 1px solid var(--accent); - border-radius: 0.5rem; - } - - article h3 { - margin-top: 0; + grid-template-columns: repeat(auto-fill,minmax(350px,1fr)); + gap: 1.5rem; } </style> |
