Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions renovate.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
}
127 changes: 127 additions & 0 deletions src/components/projects/ProjectCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
import { Icon } from "astro-icon/components";
import type { Project } from "@/data/projects";

interface Props {
project: Project;
}

const { project } = Astro.props;
const primaryLink = project.links?.[0];

const techColors: Record<string, { bg: string; text: string }> = {
Java: { bg: "#ED8B00", text: "#1f2937" },
"Spring Boot": { bg: "#6DB33F", text: "#1f2937" },
Angular: { bg: "#DD0031", text: "#fff" },
"Node.js": { bg: "#417E38", text: "#fff" },
Docker: { bg: "#0070C5", text: "#fff" },
Jenkins: { bg: "#D33833", text: "#fff" },
OpenShift: { bg: "#CC0000", text: "#fff" },
Git: { bg: "#CC3B18", text: "#fff" },
Jira: { bg: "#0052CC", text: "#fff" },
Servlets: { bg: "#ED8B00", text: "#1f2937" },
JSP: { bg: "#ED8B00", text: "#1f2937" },
Tomcat: { bg: "#CC3B00", text: "#fff" },
Apertium: { bg: "#7C3AED", text: "#fff" },
"SAP ABAP4": { bg: "#007DB8", text: "#fff" },
"SAP UI5": { bg: "#007DB8", text: "#fff" },
WebSockets: { bg: "#4338CA", text: "#fff" },
Arduino: { bg: "#007D80", text: "#fff" },
IoT: { bg: "#1565C0", text: "#fff" },
TailwindCSS: { bg: "#06B6D4", text: "#0f172a" },
"CI/CD": { bg: "#475569", text: "#fff" },
Python: { bg: "#3776AB", text: "#fff" },
PyTorch: { bg: "#C0380A", text: "#fff" },
React: { bg: "#20232A", text: "#61DAFB" },
"Machine Learning": { bg: "#6D28D9", text: "#fff" },
MongoDB: { bg: "#2E6B31", text: "#fff" },
"Discord API": { bg: "#5865F2", text: "#fff" },
TypeScript: { bg: "#3178C6", text: "#fff" },
Web: { bg: "#475569", text: "#fff" },
Astro: { bg: "#FF5D01", text: "#1f2937" },
};
---

<article
class={`border-accent-2/15 group relative flex flex-col gap-3 rounded-md border border-dashed p-4 transition-colors${primaryLink ? " hover:border-accent/60 cursor-pointer" : ""}`}
>
<header class="flex flex-col gap-1">
<h3
class={`text-accent-2 text-base font-semibold${primaryLink ? " group-hover:text-accent transition-colors" : ""}`}
>
{project.name}
</h3>
<p class="text-xs text-gray-600 dark:text-gray-400">{project.summary}</p>
</header>

<p class="text-sm leading-relaxed">{project.description}</p>

{
project.tech.length > 0 && (
<ul class="relative z-10 flex flex-wrap gap-1.5" role="list">
{project.tech.map((t) => {
const color = techColors[t] ?? { bg: "#6B7280", text: "#fff" };
return (
<li>
<button
aria-pressed="false"
class="tech-pill border-accent-2/20 cursor-pointer rounded border px-1.5 py-0.5 text-[11px] font-medium transition-all duration-150"
data-tech={t}
style={`--pill-bg: ${color.bg}; --pill-text: ${color.text};`}
type="button"
>
{t}
</button>
</li>
);
})}
</ul>
)
}

{
project.links && project.links.length > 0 && (
<ul class="relative z-10 mt-auto flex flex-wrap gap-x-4 gap-y-1 pt-1" role="list">
{project.links.map((link) => (
<li>
<a
class="cactus-link inline-flex items-center gap-1 text-xs"
href={link.href}
rel="noreferrer"
target="_blank"
>
<Icon
aria-hidden="true"
class="h-3.5 w-3.5"
focusable="false"
name="mdi:open-in-new"
/>
{link.label}
</a>
</li>
))}
</ul>
)
}

{
primaryLink && (
<a
aria-label={project.name}
class="absolute inset-0 rounded-md"
href={primaryLink.href}
rel="noreferrer"
target="_blank"
/>
)
}
</article>

<style>
.tech-pill:hover,
.tech-pill[aria-pressed="true"] {
background-color: var(--pill-bg);
color: var(--pill-text);
border-color: var(--pill-bg);
}
</style>
109 changes: 109 additions & 0 deletions src/data/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
export interface Project {
name: string;
summary: string;
description: string;
tech: string[];
links?: {
label: string;
href: string;
}[];
}

export const projects: Project[] = [
{
name: "e-RVC · Catalonia's vineyard registry",
summary: "Microservices platform for Catalonia's public wine-growing registry.",
description:
"Microservices platform behind the e-RVC, the Generalitat de Catalunya's public registry where grape growers, wineries and regulatory councils manage plantings, harvests and wine-growing potential. 22 microservices in Java 8 and Spring Boot with an Angular 14 frontend. Direct client work for requirements gathering, functional analysis and critical incident resolution. Deployed on Docker, Jenkins and OpenShift.",
tech: [
"Java",
"Spring Boot",
"Angular",
"Node.js",
"Docker",
"Jenkins",
"OpenShift",
"Git",
"Jira",
],
links: [{ label: "ervc.agricultura.gencat.cat", href: "https://ervc.agricultura.gencat.cat" }],
},
{
name: "Catalan language services platform",
summary: "Legacy Java apps powering Catalan proficiency exams, translation and resources.",
description:
"Evolution and maintenance of the legacy applications run by the Generalitat de Catalunya's language administration — VxL (Voluntariat per la Llengua, the program that pairs Catalan learners with fluent speakers), GestCerCat (Catalan proficiency exam management), LLC (Catalan language resources portal), RTIJ (sworn translators and interpreters registry) and the in-house Apertium-based machine translators for Occitan/Aranese and the rest of the supported language pairs — built on Java 7, Servlets and JSP, deployed on Tomcat 7/9.",
tech: ["Java", "Servlets", "JSP", "Tomcat", "Jenkins", "Apertium"],
links: [
{ label: "VxL", href: "https://www.vxl.cat" },
{ label: "LLC", href: "https://aplicacions.llengua.gencat.cat/llc/AppJava/index.html" },
],
},
{
name: "SAP ABAP4 · Hotel chains",
summary: "ABAP4 and UI5 customisations for the ERP of large hotel chains.",
description:
"Feature development and bug fixing in SAP ABAP4 for large hotel chains, including frontend work in SAP UI5 and integration with workflows based on Node.js 18 and Angular.",
tech: ["SAP ABAP4", "SAP UI5", "Node.js", "Angular"],
links: [{ label: "tmsforhotels.com", href: "https://www.tmsforhotels.com/tmshotels" }],
},
{
name: "Various projects at The Box of the Boss",
summary: "Web platforms and IoT stage effects for an events production company.",
description:
"Web applications and platforms for an events and shows production company, including IoT-based control systems for stage illusion effects using Arduino, WebSockets and Node.js.",
tech: ["Node.js", "WebSockets", "Arduino", "IoT"],
links: [
{ label: "theboxoftheboss.com", href: "https://theboxoftheboss.com" },
{ label: "plandefuga.es", href: "https://plandefuga.es" },
{ label: "pedrotercero.com", href: "https://pedrotercero.com" },
],
},
{
name: "GDM La Merced",
summary: "Website and back-office tooling for a local handball club.",
description:
"Development and maintenance of the website and internal tooling for a local handball club: player management, match calendars and results publishing.",
tech: ["Node.js", "Web"],
links: [{ label: "gdmlamerced.com", href: "https://gdmlamerced.com" }],
},
{
name: "usarral.com",
summary: "Personal site and technical blog, statically built with Astro.",
description:
"Personal website and technical blog focused on development, Docker deployments, security and systems automation. Static, fast and shipped through a custom CI/CD pipeline.",
tech: ["Astro", "Node.js", "TailwindCSS", "Docker", "CI/CD"],
links: [
{ label: "usarral.com", href: "https://usarral.com" },
{ label: "GitHub", href: "https://github.com/usarral/usarral.com" },
],
},
{
name: "Astra5 · NASA Space Apps Challenge",
summary: "Machine-learning flood detection on Sentinel-1 satellite imagery.",
description:
"Flood detection tool built on Sentinel-1 imagery using Machine Learning with PyTorch. Backend in Node.js, model in Python and frontend in React.",
tech: ["Node.js", "Python", "PyTorch", "React", "Machine Learning"],
links: [{ label: "astra5.usarral.com", href: "https://astra5.usarral.com" }],
},
{
name: "NodeBot",
summary: "Discord chatbot that reached 3M users across 27,000 servers.",
description:
"Scalable Discord chatbot built on Node.js and MongoDB. Reached more than 3 million users across 27,000 servers.",
tech: ["Node.js", "MongoDB", "Discord API"],
links: [
{
label: "GitHub",
href: "https://github.com/LyricalString/Node-Discord-Bot/tree/master",
},
],
},
{
name: "butler-ci-cli",
summary: "Terminal tooling to drive Jenkins pipelines from the command line.",
description: "Terminal tooling to automate Jenkins pipelines straight from the command line.",
tech: ["Node.js", "TypeScript", "Jenkins"],
links: [{ label: "GitHub", href: "https://github.com/usarral/butler-ci-cli" }],
},
];
83 changes: 83 additions & 0 deletions src/pages/projects.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
import ProjectCard from "@/components/projects/ProjectCard.astro";
import { projects } from "@/data/projects";
import PageLayout from "@/layouts/Base.astro";

const meta = {
description:
"A selection of the projects I've worked on: from microservice architectures to live IoT, Discord bots and tools I use day to day.",
title: "Projects",
};
---

<PageLayout meta={meta}>
<h1 class="title mb-4">Projects</h1>
<p class="mb-6 text-sm">
A selection of professional and personal projects I've worked on — ranging from microservice
architectures and SAP, to live IoT, Discord bots and tooling to automate my day to day.
Click on any technology pill to filter.
</p>

<div
class="border-accent-2/20 mb-6 flex items-center gap-2 rounded border border-dashed px-3 py-2 text-xs"
hidden
id="project-filter-banner"
>
<span>Filtering by</span>
<span class="text-accent-2 font-semibold" id="project-filter-label"></span>
<span class="text-gray-600 dark:text-gray-400" id="project-filter-count"></span>
<button class="cactus-link ml-auto" id="project-filter-clear" type="button">
Clear filter
</button>
</div>

<div class="grid gap-4 sm:grid-cols-2" id="projects-grid">
{projects.map((project) => <ProjectCard project={project} />)}
</div>
</PageLayout>

<script>
const banner = document.getElementById("project-filter-banner");
const label = document.getElementById("project-filter-label");
const count = document.getElementById("project-filter-count");
const clearBtn = document.getElementById("project-filter-clear");
const grid = document.getElementById("projects-grid");

if (banner && label && count && clearBtn && grid) {
let activeFilter: string | null = null;
const articles = Array.from(grid.querySelectorAll<HTMLElement>("article"));
const pills = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-tech]"));

const applyFilter = (tech: string | null) => {
activeFilter = tech;
let visible = 0;

for (const article of articles) {
const matches =
!tech ||
article.querySelector(`[data-tech="${CSS.escape(tech)}"]`) !== null;
article.hidden = !matches;
if (matches) visible += 1;
}

for (const pill of pills) {
pill.setAttribute("aria-pressed", String(pill.dataset.tech === tech));
}

banner.hidden = !tech;
if (tech) {
label.textContent = tech;
count.textContent = `· ${visible} of ${articles.length} projects`;
}
};

for (const pill of pills) {
pill.addEventListener("click", () => {
const tech = pill.dataset.tech ?? null;
applyFilter(activeFilter === tech ? null : tech);
});
}

clearBtn.addEventListener("click", () => applyFilter(null));
}
</script>
4 changes: 4 additions & 0 deletions src/site.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export const menuLinks: { path: string; title: string }[] = [
// path: "/about/",
// title: "About",
//},
{
path: "/projects/",
title: "Projects",
},
{
path: "/posts/",
title: "Blog",
Expand Down