aboutsummaryrefslogtreecommitdiff
path: root/src/layouts
diff options
context:
space:
mode:
Diffstat (limited to 'src/layouts')
-rw-r--r--src/layouts/BlogListLayout.astro325
-rw-r--r--src/layouts/Layout.astro5
-rw-r--r--src/layouts/PortfolioItemLayout.astro77
-rw-r--r--src/layouts/PortfolioPageLayout.astro110
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>