feat: add gh-dashboard project, language switch partial, header alignment fix

- Add gh-dashboard (debba/gh-dashboard) as collaboration project in EN/ES CV data
- Add head-language-switch.html template partial with test
- Change CV header text-align-last from justify to left
- Update .gitignore to exclude prompt symlinks and cv-site binary
This commit is contained in:
juanatsap
2026-05-07 22:52:46 +01:00
parent c6685e40d1
commit f3fc6a2632
7 changed files with 309 additions and 5 deletions
+6
View File
@@ -69,3 +69,9 @@ tests/screenshots/
# Personal learning documentation README (private goals and notes) # Personal learning documentation README (private goals and notes)
_go-learning/README.md _go-learning/README.md
# Personal prompt symlinks (Obsidian vault)
*-PROMPT.md
# Built binary
cv-site
+32 -1
View File
@@ -760,6 +760,37 @@
], ],
"projectID": "cdc-starter-kit" "projectID": "cdc-starter-kit"
}, },
{
"title": "gh-dashboard - Self-Hosted GitHub Analytics Dashboard",
"category": "collaboration",
"projectName": "gh-dashboard",
"projectDesc": "Self-Hosted GitHub Analytics Dashboard",
"url": "https://github.com/debba/gh-dashboard",
"gitRepoUrl": "https://github.com/debba/gh-dashboard",
"openSource": true,
"projectLogo": "",
"location": "Online",
"startDate": "2026",
"current": true,
"technologies": [
"TypeScript",
"React 19",
"Vite 8",
"Node.js",
"GitHub API",
"Vitest",
"Self-hosted"
],
"shortDescription": "Open-source contributor to <strong><a href='https://github.com/debba/gh-dashboard' target='_blank' rel='noopener noreferrer'>gh-dashboard</a></strong> by Andrea Debernardi — a self-hosted GitHub analytics dashboard with repo health scores, cross-repo issue triage, daily digests, and Kanban boards. Deployed as my personal homepage at <strong><a href='https://github.txeo.club' target='_blank' rel='noopener noreferrer'>github.txeo.club</a></strong>.",
"responsibilities": [
"Contributed 2 merged PRs (750+ lines, 21 tests) improving UX, persistence, and responsive design",
"Implemented localStorage caching for stats and filters — eliminated flash-of-zeros on page refresh",
"Fixed CSS grid responsive breakpoints and built icon-only mobile tabs with smooth hover expansion",
"Added user identity: GitHub avatar in top bar, org badges, favicon, and org-first sort order",
"Deployed and maintain a live instance on personal VPS with GitHub OAuth and SSL"
],
"projectID": "gh-dashboard"
},
{ {
"title": "Third Party Contributions", "title": "Third Party Contributions",
"category": "contrib", "category": "contrib",
@@ -1036,4 +1067,4 @@
"format": "JSON Resume Extended", "format": "JSON Resume Extended",
"language": "en" "language": "en"
} }
} }
+32 -1
View File
@@ -760,6 +760,37 @@
], ],
"projectID": "cdc-starter-kit" "projectID": "cdc-starter-kit"
}, },
{
"title": "gh-dashboard - Panel de Analíticas de GitHub Autoalojado",
"category": "collaboration",
"projectName": "gh-dashboard",
"projectDesc": "Panel de Analíticas de GitHub Autoalojado",
"url": "https://github.com/debba/gh-dashboard",
"gitRepoUrl": "https://github.com/debba/gh-dashboard",
"openSource": true,
"projectLogo": "",
"location": "Online",
"startDate": "2026",
"current": true,
"technologies": [
"TypeScript",
"React 19",
"Vite 8",
"Node.js",
"GitHub API",
"Vitest",
"Self-hosted"
],
"shortDescription": "Contribuidor open-source a <strong><a href='https://github.com/debba/gh-dashboard' target='_blank' rel='noopener noreferrer'>gh-dashboard</a></strong> de Andrea Debernardi — un panel de analíticas de GitHub autoalojado con puntuaciones de salud de repos, triaje de issues, resúmenes diarios y tableros Kanban. Desplegado como mi página personal en <strong><a href='https://github.txeo.club' target='_blank' rel='noopener noreferrer'>github.txeo.club</a></strong>.",
"responsibilities": [
"Contribuí 2 PRs fusionados (750+ líneas, 21 tests) mejorando UX, persistencia y diseño responsive",
"Implementé caché en localStorage para estadísticas y filtros — eliminé el parpadeo de ceros al recargar la página",
"Corregí breakpoints responsive de CSS grid y construí tabs móviles solo-icono con expansión suave al hacer hover",
"Añadí identidad de usuario: avatar de GitHub en la barra superior, badges de organizaciones, favicon y orden de orgs primero",
"Desplegué y mantengo una instancia en mi VPS personal con GitHub OAuth y SSL"
],
"projectID": "gh-dashboard"
},
{ {
"title": "Contribuciones a Proyectos de Terceros", "title": "Contribuciones a Proyectos de Terceros",
"category": "contrib", "category": "contrib",
@@ -1036,4 +1067,4 @@
"format": "JSON Resume Extended", "format": "JSON Resume Extended",
"language": "es" "language": "es"
} }
} }
+2 -2
View File
@@ -76,8 +76,8 @@
margin-top: 20px; margin-top: 20px;
/* Full justification - spread text across entire width */ /* Full justification - spread text across entire width */
text-align: justify; text-align: justify;
text-align-last: justify; text-align-last: left;
-moz-text-align-last: justify; -moz-text-align-last: left;
text-justify: inter-word; text-justify: inter-word;
/* Word breaking and hyphenation */ /* Word breaking and hyphenation */
word-spacing: -1px; word-spacing: -1px;
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
{{define "head-language-switch"}}
<head hx-head="merge">
<title>{{.CV.Personal.Name}} - {{.CV.SEO.PageTitle}}</title>
<meta name="title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}" hx-head="re-eval">
<meta name="description" content="{{.CV.Personal.Title}} | {{.CV.SEO.MetaDescription}}" hx-head="re-eval">
<link rel="canonical" href="{{.CanonicalURL}}" hx-head="re-eval">
<link rel="alternate" hreflang="en" href="{{.AlternateEN}}" hx-head="re-eval">
<link rel="alternate" hreflang="es" href="{{.AlternateES}}" hx-head="re-eval">
<meta property="og:title" content="{{.CV.Personal.Name}} - {{.CV.SEO.MetaTitle}}" hx-head="re-eval">
<meta property="og:description" content="{{.CV.Personal.Title}} | {{.CV.SEO.OgDescription}}" hx-head="re-eval">
<meta property="og:locale" content="{{if eq .Lang "es"}}es_ES{{else}}en_US{{end}}" hx-head="re-eval">
</head>
{{end}}
+223
View File
@@ -0,0 +1,223 @@
#!/usr/bin/env bun
/**
* HEAD-SUPPORT EXTENSION TEST
* ============================
* Tests that the head-support extension updates <head> tags
* and <html lang> on language switching via HTMX
* - Verifies <html lang> updates on language switch
* - Verifies <title> updates on language switch
* - Verifies <meta description> updates
* - Verifies no duplicate tags after multiple switches
*/
import { chromium } from 'playwright';
const URL = "http://localhost:1999";
async function testHeadSupport() {
console.log('🏷️ HEAD-SUPPORT EXTENSION TEST\n');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
const errors = [];
const testResults = [];
page.on('console', msg => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
console.log("\n1️⃣ Loading page (English default)...");
await page.goto(URL);
await page.waitForTimeout(2000);
// ========================================================================
// TEST 1: Head-support extension is loaded
// ========================================================================
console.log("\n2️⃣ Testing head-support extension loaded...");
const extLoaded = await page.evaluate(() => {
const body = document.body;
const hxExt = body.getAttribute('hx-ext');
return {
hasHxExt: hxExt !== null && hxExt.includes('head-support'),
hxExtValue: hxExt
};
});
console.log(` hx-ext attribute: ${extLoaded.hxExtValue}`);
console.log(` ${extLoaded.hasHxExt ? '✅ PASS' : '❌ FAIL'} - head-support extension activated`);
testResults.push({ test: 'Head-support Extension Loaded', passed: extLoaded.hasHxExt });
// ========================================================================
// TEST 2: Initial state - English
// ========================================================================
console.log("\n3️⃣ Testing initial state (English)...");
const initialState = await page.evaluate(() => {
return {
htmlLang: document.documentElement.getAttribute('lang'),
title: document.title,
metaDesc: document.querySelector('meta[name="description"]')?.getAttribute('content') || '',
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || ''
};
});
console.log(` <html lang>: ${initialState.htmlLang}`);
console.log(` <title>: ${initialState.title}`);
console.log(` canonical: ${initialState.canonical}`);
const initialOk = initialState.htmlLang === 'en';
console.log(` ${initialOk ? '✅ PASS' : '❌ FAIL'} - Initial state is English`);
testResults.push({ test: 'Initial English State', passed: initialOk });
// ========================================================================
// TEST 3: Switch to Spanish via HTMX button
// ========================================================================
console.log("\n4️⃣ Switching to Spanish via HTMX...");
// Click the Spanish button
const esButton = await page.$('button[aria-label="Español"]');
if (esButton) {
await esButton.click();
await page.waitForTimeout(3000); // Wait for HTMX swap + head-support processing
const spanishState = await page.evaluate(() => {
return {
htmlLang: document.documentElement.getAttribute('lang'),
title: document.title,
metaDesc: document.querySelector('meta[name="description"]')?.getAttribute('content') || '',
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || '',
ogLocale: document.querySelector('meta[property="og:locale"]')?.getAttribute('content') || ''
};
});
console.log(` <html lang>: ${spanishState.htmlLang}`);
console.log(` <title>: ${spanishState.title}`);
console.log(` canonical: ${spanishState.canonical}`);
console.log(` og:locale: ${spanishState.ogLocale}`);
const langChanged = spanishState.htmlLang === 'es';
const canonicalChanged = spanishState.canonical.includes('lang=es');
const descChanged = spanishState.metaDesc.length > 0;
console.log(` ${langChanged ? '✅ PASS' : '❌ FAIL'} - <html lang> changed to "es"`);
console.log(` ${canonicalChanged ? '✅ PASS' : '❌ FAIL'} - canonical URL updated to Spanish`);
console.log(` ${descChanged ? '✅ PASS' : '❌ FAIL'} - meta description present`);
testResults.push({ test: 'HTML Lang Updated to ES', passed: langChanged });
testResults.push({ test: 'Canonical URL Updated to ES', passed: canonicalChanged });
testResults.push({ test: 'Meta Description Present', passed: descChanged });
} else {
console.log(' ❌ FAIL - Spanish button not found');
testResults.push({ test: 'HTML Lang Updated to ES', passed: false });
testResults.push({ test: 'Canonical URL Updated to ES', passed: false });
testResults.push({ test: 'Meta Description Present', passed: false });
}
// ========================================================================
// TEST 4: Switch back to English
// ========================================================================
console.log("\n5️⃣ Switching back to English...");
const enButton = await page.$('button[aria-label="English"]');
if (enButton) {
await enButton.click();
await page.waitForTimeout(3000);
const englishState = await page.evaluate(() => {
return {
htmlLang: document.documentElement.getAttribute('lang'),
title: document.title,
canonical: document.querySelector('link[rel="canonical"]')?.getAttribute('href') || '',
ogLocale: document.querySelector('meta[property="og:locale"]')?.getAttribute('content') || ''
};
});
console.log(` <html lang>: ${englishState.htmlLang}`);
console.log(` <title>: ${englishState.title}`);
console.log(` canonical: ${englishState.canonical}`);
console.log(` og:locale: ${englishState.ogLocale}`);
const langBack = englishState.htmlLang === 'en';
const canonicalBack = englishState.canonical.includes('lang=en');
console.log(` ${langBack ? '✅ PASS' : '❌ FAIL'} - <html lang> back to "en"`);
console.log(` ${canonicalBack ? '✅ PASS' : '❌ FAIL'} - canonical URL back to English`);
testResults.push({ test: 'HTML Lang Restored to EN', passed: langBack });
testResults.push({ test: 'Canonical URL Restored to EN', passed: canonicalBack });
} else {
console.log(' ❌ FAIL - English button not found');
testResults.push({ test: 'HTML Lang Restored to EN', passed: false });
testResults.push({ test: 'Canonical URL Restored to EN', passed: false });
}
// ========================================================================
// TEST 5: No duplicate tags after multiple switches
// ========================================================================
console.log("\n6️⃣ Testing no duplicate tags after multiple switches...");
const duplicateCheck = await page.evaluate(() => {
const titles = document.querySelectorAll('title').length;
const descriptions = document.querySelectorAll('meta[name="description"]').length;
const canonicals = document.querySelectorAll('link[rel="canonical"]').length;
const ogLocales = document.querySelectorAll('meta[property="og:locale"]').length;
return {
titles,
descriptions,
canonicals,
ogLocales
};
});
console.log(` <title> tags: ${duplicateCheck.titles}`);
console.log(` <meta description> tags: ${duplicateCheck.descriptions}`);
console.log(` <link canonical> tags: ${duplicateCheck.canonicals}`);
console.log(` og:locale tags: ${duplicateCheck.ogLocales}`);
const noDuplicates = duplicateCheck.titles === 1 &&
duplicateCheck.descriptions === 1 &&
duplicateCheck.canonicals === 1 &&
duplicateCheck.ogLocales === 1;
console.log(` ${noDuplicates ? '✅ PASS' : '❌ FAIL'} - No duplicate tags`);
testResults.push({ test: 'No Duplicate Tags', passed: noDuplicates });
// ========================================================================
// FINAL SUMMARY
// ========================================================================
console.log("\n" + "=".repeat(70));
console.log("📊 TEST SUMMARY\n");
const totalTests = testResults.length;
const passedTests = testResults.filter(r => r.passed).length;
const failedTests = totalTests - passedTests;
testResults.forEach(result => {
console.log(` ${result.passed ? '✅' : '❌'} ${result.test}`);
});
console.log(`\n Total: ${passedTests}/${totalTests} tests passed`);
if (errors.length === 0) {
console.log("\n✅ NO CONSOLE ERRORS");
} else {
console.log(`\n⚠️ ${errors.length} CONSOLE ERRORS`);
}
console.log("=".repeat(70) + "\n");
await browser.close();
if (failedTests === 0) {
console.log("🎉 HEAD-SUPPORT EXTENSION VALIDATED!\n");
process.exit(0);
} else {
console.log("⚠️ SOME TESTS FAILED - See details above\n");
process.exit(1);
}
}
await testHeadSupport();