summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-06-07 20:17:01 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2025-06-07 20:17:01 +0200
commit6cb688bd1b2285fb917194852fdc285c798d43cc (patch)
tree535293539e53dfa3ec3aeca8f217b6d066350c75
parent36d3a2c4c6dbfcc74271d0956eff9b1e454fc138 (diff)
Add new images and update portfolio layouts for enhanced presentation
-rw-r--r--public/images/portfolio/mapa-ferrol/home-info-narrow.pngbin0 -> 882480 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/home-info-narrow.webpbin0 -> 46338 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/home-narrow.pngbin0 -> 442577 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/home-narrow.webpbin0 -> 27500 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/planner-narrow.pngbin0 -> 248435 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/planner-narrow.webpbin0 -> 30408 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/route-id-narrow.pngbin0 -> 913156 bytes
-rw-r--r--public/images/portfolio/mapa-ferrol/route-id-narrow.webpbin0 -> 54736 bytes
-rw-r--r--src/content.config.ts4
-rw-r--r--src/data/portfolio/mapa-ferrol.mdx37
-rw-r--r--src/layouts/BlogSingleLayout.astro134
-rw-r--r--src/layouts/Layout.astro1
-rw-r--r--src/layouts/PortfolioItemLayout.astro77
-rw-r--r--src/layouts/PortfolioListLayout.astro (renamed from src/layouts/PortfolioPageLayout.astro)0
-rw-r--r--src/layouts/PortfolioSingleLayout.astro370
-rw-r--r--src/pages/blog/[id].astro125
-rw-r--r--src/pages/portfolio/[id].astro4
-rw-r--r--src/pages/portfolio/index.astro4
-rw-r--r--styles/shared.scss5
19 files changed, 551 insertions, 210 deletions
diff --git a/public/images/portfolio/mapa-ferrol/home-info-narrow.png b/public/images/portfolio/mapa-ferrol/home-info-narrow.png
new file mode 100644
index 0000000..5e9a0de
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/home-info-narrow.png
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/home-info-narrow.webp b/public/images/portfolio/mapa-ferrol/home-info-narrow.webp
new file mode 100644
index 0000000..9a24c99
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/home-info-narrow.webp
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/home-narrow.png b/public/images/portfolio/mapa-ferrol/home-narrow.png
new file mode 100644
index 0000000..52f2ffe
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/home-narrow.png
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/home-narrow.webp b/public/images/portfolio/mapa-ferrol/home-narrow.webp
new file mode 100644
index 0000000..c060969
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/home-narrow.webp
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/planner-narrow.png b/public/images/portfolio/mapa-ferrol/planner-narrow.png
new file mode 100644
index 0000000..62386ba
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/planner-narrow.png
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/planner-narrow.webp b/public/images/portfolio/mapa-ferrol/planner-narrow.webp
new file mode 100644
index 0000000..3644a8e
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/planner-narrow.webp
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/route-id-narrow.png b/public/images/portfolio/mapa-ferrol/route-id-narrow.png
new file mode 100644
index 0000000..92ec224
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/route-id-narrow.png
Binary files differ
diff --git a/public/images/portfolio/mapa-ferrol/route-id-narrow.webp b/public/images/portfolio/mapa-ferrol/route-id-narrow.webp
new file mode 100644
index 0000000..d78622e
--- /dev/null
+++ b/public/images/portfolio/mapa-ferrol/route-id-narrow.webp
Binary files differ
diff --git a/src/content.config.ts b/src/content.config.ts
index f3d7755..acaaf8a 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -20,7 +20,9 @@ const portfolio = defineCollection({
githubLink: z.string().url().optional(),
onlineLink: z.string().url().optional(),
demoLink: z.string().url().optional(),
- images: z.array(z.string()).default([])
+ images: z.array(
+ z.object({ src: z.string(), alt: z.string() })
+ ).default([])
}),
});
diff --git a/src/data/portfolio/mapa-ferrol.mdx b/src/data/portfolio/mapa-ferrol.mdx
index 144ac62..6152ced 100644
--- a/src/data/portfolio/mapa-ferrol.mdx
+++ b/src/data/portfolio/mapa-ferrol.mdx
@@ -3,16 +3,43 @@ title: "Mapa de patrimonio da Costa Ártabra"
description: "Desarrollo de una aplicación completa para el patrimonio cultural y militar de la Costa Ártabra."
technologies: ["dotnet", "react", "mysql", "ubuntu"]
onlineLink: "https://mapacostaartabramilitar.com"
+images:
+- src: "/images/portfolio/mapa-ferrol/home-narrow"
+ alt: "Vista principal del mapa con los elementos del patrimonio"
+- src: "/images/portfolio/mapa-ferrol/home-info-narrow"
+ alt: "Sheet con información de un elemento patrimonial"
+- src: "/images/portfolio/mapa-ferrol/route-id-narrow"
+ alt: "Vista de una ruta predefinida"
+- src: "/images/portfolio/mapa-ferrol/planner-narrow"
+ alt: "Planificador de rutas de senderismo"
---
-Se desarrolló una aplicación web completa para la gestión del patrimonio cultural y militar de la Costa Ártabra, en Galicia, España. Esta aplicación permite a los usuarios explorar y descubrir el patrimonio de la región (Ferrol, Ares y Valdoviño) incluyendo las baterías militares, faros, iglesias, miradores, playas, etc.
+Desarrollo de una **aplicación web integral** para la exploración del patrimonio cultural y militar de la Costa Ártabra (Ferrol, Ares, Valdoviño). La plataforma incluye baterías costeras, faros, rutas de senderismo y puntos de interés natural, ofreciendo una experiencia adaptada principalmente a dispositivos móviles.
-La aplicación está desarrollada con ASP.NET Core y React, utilizando una base de datos MySQL para almacenar la información del patrimonio y permitir al personal de la administración añadir, editar y eliminar elementos del patrimonio.
+Tecnologías clave:
-Para la parte del frontend, se utilizó React con TypeScript, creando una interfaz de usuario intuitiva y fluida, adaptándose primordialmente a dispositivos móviles. Además, se implementó un mapa interactivo utilizando MapLibre y capas de OpenStreetMap (mapa base) y el PNOA (Satélite del IGN) para mostrar la ubicación de los elementos del patrimonio.
+* Backend con ASP.NET Core y base de datos MySQL.
+* Frontend en React (TypeScript) y mapas interactivos con MapLibre.
+* Teselas vectoriales de OpenStreetMap con [Protomaps](https://protomaps.com/) para un mapa de alta calidad y buen rendimiento.
+* Calculador de rutas autoalojado con [OSRM](https://project-osrm.org/) para optimización de rutas.
-La aplicación también incluye un sistema de rutas de senderismo, tanto proporcionadas por la administración como generadas a medida por los usuarios, permitiendo a los visitantes explorar la región a su manera.
+Diseñada para autonomía y privacidad:
-Una complejidad destacable fue hacer la aplicación rápida y fluida para el usuario, para garantizar la mejor experiencia posible al navegar por el patrimonio, sobre todo desde dispositivos móviles en redes móviles.
+* Infraestructura autoalojada (Ubuntu) sin dependencia de APIs comerciales.
+* Optimización de rutas con motor de cálculo interno (OSRM).
+* Panel de administración para gestión centralizada del patrimonio.
+
+## Características clave
+
+1. **Tecnologías autónomas**: Implementación sin dependencia de servicios externos (como Google Maps), garantizando privacidad del usuario y reducción de costos.
+
+2. **Gestión de recursos**: Solución personalizada para iconografía y capas de mapa, asegurando consistencia visual y eficiencia.
+
+3. **Planificación de rutas inteligente**: Sistema de optimización para senderismo basado en puntos patrimoniales, con énfasis en rendimiento y estabilidad.
+
+4. **Panel de administración**: Herramienta interna para gestión de contenido por personal no técnico.
+
+
+---
Desarrollado en 2025 en colaboración con [Kendra](https://kendra.es/).
diff --git a/src/layouts/BlogSingleLayout.astro b/src/layouts/BlogSingleLayout.astro
new file mode 100644
index 0000000..0c3b934
--- /dev/null
+++ b/src/layouts/BlogSingleLayout.astro
@@ -0,0 +1,134 @@
+---
+import Layout from "@/layouts/Layout.astro";
+import { getCollection, render } from "astro:content";
+import { type GetStaticPaths } from "astro";
+
+interface Props {
+ entry: any;
+}
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const entries = await getCollection("blog");
+ return entries.map((entry: any) => ({
+ params: { id: entry.id },
+ props: { entry },
+ }));
+};
+
+const { entry } = Astro.props;
+const { Content } = await render(entry);
+const formattedDate = new Date(entry.data.publishedAt).toLocaleDateString(
+ "es-ES",
+ {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ weekday: "long",
+ },
+);
+
+const schema = {
+ "@context": "https://schema.org",
+ "@type": "BlogPosting",
+ headline: entry.data.title,
+ datePublished: entry.data.publishedAt.toISOString(),
+ keywords: entry.data.tags || [],
+ author: {
+ "@type": "Person",
+ name: "Ariel Costas Guerrero",
+ },
+ publisher: {
+ "@type": "Person",
+ name: "Ariel Costas Guerrero",
+ url: "https://www.costas.dev",
+ image: {
+ "@type": "ImageObject",
+ url: "https://www.costas.dev/favicon.png",
+ },
+ },
+};
+---
+
+<Layout title={entry.data.title} description={entry.data.metaDescription}>
+ <script
+ is:inline
+ type="application/ld+json"
+ slot="head-jsonld"
+ set:html={JSON.stringify(schema)}
+ />
+
+ <h1>{entry.data.title}</h1>
+ <small>
+ Publicado el
+ <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 />
+
+ <p>
+ <a href="/blog">Volver al blog</a>
+ </p>
+</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/layouts/Layout.astro b/src/layouts/Layout.astro
index d23d212..20dca59 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -181,7 +181,6 @@ const { title, description, empty } = Astro.props;
main a,
footer a {
text-decoration: none;
- padding: 0.1rem;
box-shadow: 0 1px $accent;
&:hover {
diff --git a/src/layouts/PortfolioItemLayout.astro b/src/layouts/PortfolioItemLayout.astro
deleted file mode 100644
index 95715cb..0000000
--- a/src/layouts/PortfolioItemLayout.astro
+++ /dev/null
@@ -1,77 +0,0 @@
----
-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/PortfolioListLayout.astro
index b894f9f..b894f9f 100644
--- a/src/layouts/PortfolioPageLayout.astro
+++ b/src/layouts/PortfolioListLayout.astro
diff --git a/src/layouts/PortfolioSingleLayout.astro b/src/layouts/PortfolioSingleLayout.astro
new file mode 100644
index 0000000..8ca8cbb
--- /dev/null
+++ b/src/layouts/PortfolioSingleLayout.astro
@@ -0,0 +1,370 @@
+---
+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>
+
+ <small>Hecho con {data.technologies.map((technology: string) => (
+ <TechnologyBadge size="small" code={technology} />
+ ))}</small>
+
+ <Content />
+
+ <div class="project-links">
+ {data.githubLink && <a href={data.githubLink} target="_blank" rel="noopener noreferrer" class="project-link">
+ <Icon name="tabler:brand-github" class="link-icon"/>
+ <span>Código en GitHub</span>
+ </a>}
+
+ {data.onlineLink && <a href={data.onlineLink} target="_blank" rel="noopener noreferrer" class="project-link">
+ <Icon name="tabler:external-link" class="link-icon"/>
+ <span>Ver en línea</span>
+ </a>}
+
+ {data.demoLink && <a href={data.demoLink} target="_blank" rel="noopener noreferrer" class="project-link">
+ <Icon name="tabler:device-laptop" class="link-icon"/>
+ <span>Ver demo</span>
+ </a>}
+ </div>
+
+ {data.images.length > 0 && (
+ <h2>Galería</h2>
+ <section id="project-images">
+ {data.images.map((image) => (
+ <a href={`${image.src}.png`} target="_blank" rel="noopener noreferrer">
+ <picture>
+ <source
+ srcset={`${image.src}.webp`}
+ type="image/webp"
+ />
+ <img src={`${image.src}.png`} alt={image.alt} loading="lazy" />
+ </picture>
+ </a>
+ ))}
+ </section>
+ )}
+
+ <dialog id="largeimage-dialog">
+ <div id="largeimage-dialogcontainer">
+ <button class="largeimage__nav" aria-label="Previous image" id="largeimage__nav-left">
+ <Icon name="tabler:chevron-left" />
+ </button>
+ <div id="largeimage-imagecontainer">
+ <img id="largeimage-image" alt="Some alt" />
+ </div>
+ <div id="largeimage-caption">Some caption</div>
+ <button class="largeimage__nav" aria-label="Next image" id="largeimage__nav-right">
+ <Icon name="tabler:chevron-right" />
+ </button>
+ <button id="largeimage-close"><Icon name="tabler:x" /></button>
+ </div>
+ </dialog>
+
+</Layout>
+
+<script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const dialog = document.getElementById("largeimage-dialog") as HTMLDialogElement;
+ const closeButton = document.getElementById("largeimage-close") as HTMLButtonElement;
+ const imageElement = document.getElementById("largeimage-image") as HTMLImageElement;
+ const captionElement = document.getElementById("largeimage-caption") as HTMLDivElement;
+ const navLeft = document.getElementById("largeimage__nav-left") as HTMLButtonElement;
+ const navRight = document.getElementById("largeimage__nav-right") as HTMLButtonElement;
+ const imageLinks = Array.from(document.querySelectorAll("#project-images a")) as HTMLAnchorElement[];
+
+ const images = imageLinks.map(link => {
+ const img = link.querySelector("img");
+ return {
+ src: img?.src,
+ alt: img?.alt
+ };
+ });
+ let currentIndex = 0;
+
+ function showImage(index: number) {
+ if (index < 0 || index >= images.length) return;
+ currentIndex = index;
+ const { src, alt } = images[currentIndex];
+ if (!imageElement || !captionElement) {
+ console.error("Image or caption element not found");
+ return;
+ }
+ imageElement.src = src!;
+ imageElement.alt = alt!;
+ captionElement.textContent = alt || "Imagen del proyecto";
+ }
+
+ if (!dialog || !closeButton || !imageElement || !captionElement) {
+ console.error("Dialog or elements not found");
+ return;
+ }
+
+ imageLinks.forEach((link, idx) => {
+ link.addEventListener("click", (e) => {
+ e.preventDefault();
+ showImage(idx);
+ dialog.showModal();
+ });
+ });
+
+ closeButton.addEventListener("click", () => {
+ dialog.close();
+ });
+
+ if (navLeft) {
+ navLeft.addEventListener("click", (e) => {
+ e.preventDefault();
+ showImage((currentIndex - 1 + images.length) % images.length);
+ });
+ }
+ if (navRight) {
+ navRight.addEventListener("click", (e) => {
+ e.preventDefault();
+ showImage((currentIndex + 1) % images.length);
+ });
+ }
+
+ dialog.addEventListener("keydown", (e) => {
+ if (e.key === "ArrowLeft") {
+ showImage((currentIndex - 1 + images.length) % images.length);
+ } else if (e.key === "ArrowRight") {
+ showImage((currentIndex + 1) % images.length);
+ } else if (e.key === "Escape") {
+ dialog.close();
+ }
+ });
+
+ dialog.addEventListener("shown", () => {
+ dialog.focus();
+ });
+
+ dialog.addEventListener("open", () => {
+ dialog.focus();
+ });
+
+ dialog.addEventListener("click", (e) => {
+ if (e.target === dialog) {
+ dialog.close();
+ }
+ });
+ });
+</script>
+
+<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;
+ }
+
+ #project-images {
+ display: flex;
+ flex-direction: row;
+ gap: 1.5rem;
+ margin: 1.5rem 0 2rem;
+ overflow-x: auto;
+ padding-bottom: 1rem;
+ -webkit-overflow-scrolling: touch;
+ scrollbar-width: thin;
+ }
+
+ #project-images a {
+ display: block;
+ flex: 0 0 auto;
+ box-shadow: none;
+ text-decoration: none;
+ padding: 0;
+ }
+
+ #project-images picture {
+ display: block;
+ overflow: hidden;
+ border-radius: 0.25rem;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ height: max(20vh, 400px);
+ }
+
+ #project-images img {
+ height: 100%;
+ object-fit: cover;
+ }
+
+ #project-images a {
+ position: relative;
+ }
+
+ .project-links {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin: 1.5rem 0 2.5rem;
+ }
+
+ .project-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1.25rem;
+ border-radius: 2rem;
+ background-color: #f3f4f6;
+ color: #333;
+ font-weight: 600;
+ text-decoration: none;
+ box-shadow: none;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ background-color: #e5e7eb;
+ transform: translateY(-2px);
+ box-shadow: none;
+ }
+
+ &:focus {
+ outline: 2px solid $secondary;
+ background-color: #e5e7eb;
+ box-shadow: none;
+ }
+
+ .link-icon {
+ font-size: 1.2rem;
+ }
+ }
+
+ #largeimage-dialog {
+ padding: 0;
+ border: none;
+ background: transparent;
+ max-width: 100vw;
+ max-height: 100vh;
+ margin: auto;
+ }
+
+ #largeimage-dialog::backdrop {
+ background: #000c;
+ }
+
+ #largeimage-dialogcontainer {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ height: 100%;
+ padding: 1.5rem;
+ }
+
+ #largeimage-imagecontainer {
+ max-width: 100%;
+ max-height: calc(100vh - 2rem);
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #largeimage-image {
+ object-fit: cover;
+ max-width: 100%;
+ max-height: calc(100vh - 9rem);
+
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ #largeimage-caption {
+ position: static;
+ background: #FFFC;
+ color: #000;
+ font-size: 1rem;
+ text-align: center;
+ border-radius: 0.5rem;
+ padding: 0.75rem 1rem;
+ max-width: 100%;
+ }
+
+ #largeimage-close {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ width: 2rem;
+ height: 2rem;
+ border: none;
+ background: #fffc;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ color: #000;
+ transition: background-color .2s;
+ }
+
+ .largeimage__nav {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 2.5rem;
+ height: 2.5rem;
+ border: none;
+ background: #fffc;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ z-index: 2;
+ color: #000;
+ transition: background-color .2s;
+ }
+
+ #largeimage__nav-left {
+ left: 1rem;
+ }
+ #largeimage__nav-right {
+ right: 1rem;
+ }
+
+ .largeimage__nav:hover,
+ #largeimage-close:hover {
+ background-color: #FFFFFF;
+ }
+</style>
diff --git a/src/pages/blog/[id].astro b/src/pages/blog/[id].astro
index 3dc324a..8f02b65 100644
--- a/src/pages/blog/[id].astro
+++ b/src/pages/blog/[id].astro
@@ -1,11 +1,7 @@
---
-import Layout from "@/layouts/Layout.astro";
-import { getCollection, render } from "astro:content";
-import { type GetStaticPaths } from "astro";
-
-interface Props {
- entry: any;
-}
+import type { GetStaticPaths } from "astro";
+import { getCollection } from "astro:content";
+import BlogSingleLayout from "@/layouts/BlogSingleLayout.astro";
export const getStaticPaths: GetStaticPaths = async () => {
const entries = await getCollection("blog");
@@ -16,119 +12,6 @@ export const getStaticPaths: GetStaticPaths = async () => {
};
const { entry } = Astro.props;
-const { Content } = await render(entry);
-const formattedDate = new Date(entry.data.publishedAt).toLocaleDateString(
- "es-ES",
- {
- year: "numeric",
- month: "long",
- day: "numeric",
- weekday: "long",
- },
-);
-
-const schema = {
- "@context": "https://schema.org",
- "@type": "BlogPosting",
- headline: entry.data.title,
- datePublished: entry.data.publishedAt.toISOString(),
- keywords: entry.data.tags || [],
- author: {
- "@type": "Person",
- name: "Ariel Costas Guerrero",
- },
- publisher: {
- "@type": "Person",
- name: "Ariel Costas Guerrero",
- url: "https://www.costas.dev",
- image: {
- "@type": "ImageObject",
- url: "https://www.costas.dev/favicon.png",
- },
- },
-};
---
-<Layout title={entry.data.title} description={entry.data.metaDescription}>
- <script
- is:inline
- type="application/ld+json"
- slot="head-jsonld"
- set:html={JSON.stringify(schema)}
- />
-
- <h1>{entry.data.title}</h1>
- <small>
- Publicado el
- <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 />
-
- <p>
- <a href="/blog">Volver al blog</a>
- </p>
-</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>
+<BlogSingleLayout entry={entry} />
diff --git a/src/pages/portfolio/[id].astro b/src/pages/portfolio/[id].astro
index 601ff84..7424b91 100644
--- a/src/pages/portfolio/[id].astro
+++ b/src/pages/portfolio/[id].astro
@@ -1,7 +1,7 @@
---
import type { GetStaticPaths } from "astro";
import { getCollection } from "astro:content";
-import PortfolioItemLayout from "@/layouts/PortfolioItemLayout.astro";
+import PortfolioSingleLayout from "@/layouts/PortfolioSingleLayout.astro";
export const getStaticPaths: GetStaticPaths = async () => {
const entries = await getCollection("portfolio");
@@ -14,4 +14,4 @@ export const getStaticPaths: GetStaticPaths = async () => {
const { entry } = Astro.props;
---
-<PortfolioItemLayout entry={entry} />
+<PortfolioSingleLayout entry={entry} />
diff --git a/src/pages/portfolio/index.astro b/src/pages/portfolio/index.astro
index f5f1250..c12ab23 100644
--- a/src/pages/portfolio/index.astro
+++ b/src/pages/portfolio/index.astro
@@ -1,5 +1,5 @@
---
-import PortfolioPageLayout from "@/layouts/PortfolioPageLayout.astro";
+import PortfolioListLayout from "@/layouts/PortfolioListLayout.astro";
---
-<PortfolioPageLayout />
+<PortfolioListLayout />
diff --git a/styles/shared.scss b/styles/shared.scss
index 5844332..6078053 100644
--- a/styles/shared.scss
+++ b/styles/shared.scss
@@ -7,7 +7,6 @@
%heading {
font-family: $titleFontStack;
- line-height: 1.55;
margin-block-start: 0.75em;
margin-block-end: 0.25em;
}
@@ -15,19 +14,23 @@
h1 {
@extend %heading;
font-size: 3.55rem;
+ line-height: 1.3;
}
h2 {
@extend %heading;
font-size: 2.75rem;
+ line-height: 1.55;
}
h3 {
@extend %heading;
font-size: 2rem;
+ line-height: 1.55;
}
h4 {
@extend %heading;
font-size: 1.25rem;
+ line-height: 1.55;
}