diff --git a/api/widget.js b/api/widget.js
index a9e6eb9..d933d17 100644
--- a/api/widget.js
+++ b/api/widget.js
@@ -50,6 +50,7 @@ const CACHE_POLICIES = {
// Fully static content — refresh every 24 hours
flag: 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600',
+ techstack: 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=3600',
};
/** Default fallback for any future widget types not yet in CACHE_POLICIES. */
@@ -137,4 +138,4 @@ module.exports = async (req, res) => {
res.status(500).setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.end(``);
}
-};
+};
\ No newline at end of file
diff --git a/lib/widgets.js b/lib/widgets.js
index eb20426..27053c6 100644
--- a/lib/widgets.js
+++ b/lib/widgets.js
@@ -2533,10 +2533,89 @@ function normalizeParams(q) {
bio: truncateHtml(q.bio || 'Building cool things with code. Open-source enthusiast.', MAX_LENGTHS.bio),
skills: q.skills || 'HTML,CSS,JS,GIT,SQL,REACT,NODE,PYTHON',
- handle: q.handle || ''
+ handle: q.handle || '',
+ techs: q.techs || '',
+ layout: q.layout || 'grid',
};
}
+function renderTechStack(p) {
+ const th = theme(p.theme);
+ const title = escXml(p.label || 'Tech Stack');
+ const layout = p.layout || 'grid';
+
+ const TECH_DB = {
+ react: { label: 'React', color: '#61dafb', bg: '#20232a' },
+ nodejs: { label: 'Node.js', color: '#68a063', bg: '#1a1a1a' },
+ typescript: { label: 'TypeScript', color: '#3178c6', bg: '#1a1a1a' },
+ javascript: { label: 'JavaScript', color: '#f7df1e', bg: '#1a1a1a' },
+ python: { label: 'Python', color: '#3572a5', bg: '#1a1a1a' },
+ postgresql: { label: 'PostgreSQL', color: '#336791', bg: '#1a1a1a' },
+ mongodb: { label: 'MongoDB', color: '#47a248', bg: '#1a1a1a' },
+ docker: { label: 'Docker', color: '#2496ed', bg: '#1a1a1a' },
+ redis: { label: 'Redis', color: '#dc382d', bg: '#1a1a1a' },
+ nextjs: { label: 'Next.js', color: '#ffffff', bg: '#000000' },
+ tailwind: { label: 'Tailwind', color: '#38bdf8', bg: '#1a1a1a' },
+ graphql: { label: 'GraphQL', color: '#e10098', bg: '#1a1a1a' },
+ rust: { label: 'Rust', color: '#dea584', bg: '#1a1a1a' },
+ go: { label: 'Go', color: '#00add8', bg: '#1a1a1a' },
+ java: { label: 'Java', color: '#f89820', bg: '#1a1a1a' },
+ kotlin: { label: 'Kotlin', color: '#7f52ff', bg: '#1a1a1a' },
+ swift: { label: 'Swift', color: '#fa7343', bg: '#1a1a1a' },
+ vue: { label: 'Vue', color: '#42b883', bg: '#1a1a1a' },
+ angular: { label: 'Angular', color: '#dd0031', bg: '#1a1a1a' },
+ mysql: { label: 'MySQL', color: '#4479a1', bg: '#1a1a1a' },
+ aws: { label: 'AWS', color: '#ff9900', bg: '#232f3e' },
+ git: { label: 'Git', color: '#f05032', bg: '#1a1a1a' },
+ linux: { label: 'Linux', color: '#fcc624', bg: '#1a1a1a' },
+ figma: { label: 'Figma', color: '#a259ff', bg: '#1a1a1a' },
+ };
+
+ const techList = p.techs
+ ? String(p.techs).split(',').map(t => t.trim().toLowerCase()).filter(Boolean)
+ : ['react', 'nodejs', 'typescript', 'python', 'docker', 'postgresql'];
+
+ const techs = techList.map(k => TECH_DB[k] || { label: k, color: '#888888', bg: '#1a1a1a' });
+
+ const cols = layout === 'list' ? 1 : 3;
+ const badgeW = layout === 'list' ? 260 : 130;
+ const badgeH = 38;
+ const gapX = 12;
+ const gapY = 10;
+ const padX = 20;
+ const padTop = 52;
+
+ const rows = Math.ceil(techs.length / cols);
+ const svgW = padX * 2 + cols * badgeW + (cols - 1) * gapX;
+ const svgH = padTop + rows * badgeH + (rows - 1) * gapY + 20;
+
+ const badges = techs.map((tech, i) => {
+ const col = i % cols;
+ const row = Math.floor(i / cols);
+ const x = padX + col * (badgeW + gapX);
+ const y = padTop + row * (badgeH + gapY);
+ return `
+
+
+ ${escXml(tech.label)}
+ `;
+ }).join('');
+
+ return svgWrap(svgW, svgH, `
+
+ ◆ ${title.toUpperCase()}
+
+ ${badges}
+ `);
+}
+
async function renderWidget(type, query) {
const p = normalizeParams(query || {});
switch ((type || '').toLowerCase()) {
@@ -2554,6 +2633,7 @@ async function renderWidget(type, query) {
case 'marker': return await renderMarker(p);
case 'glass': return await renderGlass(p);
case 'countdown': return renderCountdown(p);
+ case 'techstack': return renderTechStack(p);
default:
return svgWrap(320, 60, `