Skip to content
Open
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
3 changes: 2 additions & 1 deletion api/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -137,4 +138,4 @@ module.exports = async (req, res) => {
res.status(500).setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
res.end(`<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" width="320" height="60"><rect width="320" height="60" fill="#7f1d1d"/><text x="16" y="36" fill="#fff" font-family="monospace" font-size="12">Render error: ${String(err && err.message || err).replace(/[<>&]/g,'')}</text></svg>`);
}
};
};
82 changes: 81 additions & 1 deletion lib/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `
<rect x="${x}" y="${y}" width="${badgeW}" height="${badgeH}" rx="8"
fill="${tech.bg}" stroke="${tech.color}" stroke-width="1.2"/>
<rect x="${x}" y="${y}" width="6" height="${badgeH}" rx="4"
fill="${tech.color}"/>
<text x="${x + 16}" y="${y + 24}" font-size="12" font-weight="700"
font-family="'JetBrains Mono', ui-monospace, monospace"
fill="${tech.color}">${escXml(tech.label)}</text>
`;
}).join('');

return svgWrap(svgW, svgH, `
<rect width="${svgW}" height="${svgH}" rx="12" fill="${th.bg}"/>
<text x="${padX}" y="28" font-size="10" font-weight="700" letter-spacing="2"
font-family="'JetBrains Mono', ui-monospace, monospace"
fill="${th.fg}" opacity="0.65">◆ ${title.toUpperCase()}</text>
<line x1="${padX}" y1="38" x2="${svgW - padX}" y2="38"
stroke="${th.fg}" stroke-width="0.5" opacity="0.2"/>
${badges}
`);
}

async function renderWidget(type, query) {
const p = normalizeParams(query || {});
switch ((type || '').toLowerCase()) {
Expand All @@ -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, `
<rect x="1" y="1" width="318" height="58" fill="#1a1a1a" stroke="#c8402c" stroke-width="2"/>
Expand Down