From d21593b3dc913826d4b84ff177eda22879d29b6b Mon Sep 17 00:00:00 2001 From: jhnavi25 <24cs10jh66@mitsgwl.ac.in> Date: Tue, 9 Jun 2026 11:18:14 +0530 Subject: [PATCH] feat: add Tech Stack/Dependencies Widget (#80) --- api/widget.js | 3 +- lib/widgets.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 2 deletions(-) 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(`Render error: ${String(err && err.message || err).replace(/[<>&]/g,'')}`); } -}; +}; \ 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, `