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
13 changes: 6 additions & 7 deletions api/widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,26 @@ const CACHE_POLICIES = {
weather: 'public, max-age=1800, s-maxage=1800, stale-while-revalidate=600',

// Daily-change widgets — refresh every hour

date: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
quote: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
word: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
streak: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
profile: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',

date: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
quote: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
word: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
streak: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
profile: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
marker: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
glass: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
countdown: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',

marketplace:'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
youtube: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',

marketplace: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
progress: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
extension: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',

youtube: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',
contributors: 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=600',


// Static mock content — refresh every 5 minutes
music: 'public, max-age=300, s-maxage=300, stale-while-revalidate=120',

Expand Down
97 changes: 90 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
avatarSize: 50,
showCount: true
};
let weatherPreviewSeq = 0;
let weatherPreviewKey = '';
let currentPreviewWidget = '';

// ========== UTIL ==========
function $(id){return document.getElementById(id);}
Expand All @@ -853,10 +856,17 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
}
function scrollToId(id){ const el=$(id); if(el){el.scrollIntoView({behavior:'smooth',block:'start'});} }
function pad(n){return String(n).padStart(2,'0');}

function escXml(s){
return String(s == null ? '' : s)
.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
.replace(/"/g,'&quot;').replace(/'/g,'&apos;');

function escXml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&apos;');

}
function getTzTime(tz){
try{
Expand Down Expand Up @@ -1106,6 +1116,9 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
<label class="label-tag">Theme</label>
${themeSwatches()}
</div>
<div class="note-box">
<strong>Live data:</strong> The main preview and generated README embed fetch current weather from Open-Meteo. Gallery thumbnails use sample data only.
</div>
`;
}
else if (state.widget==='weather'){
Expand Down Expand Up @@ -2903,19 +2916,58 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">\n${svgBody}\n</svg>`;
}

function svgWeather(){
async function fetchWeatherPreview(city, tempUnit) {
if (!city) return null;
try {
const geoUrl = new URL('https://geocoding-api.open-meteo.com/v1/search');
geoUrl.searchParams.set('name', city);
geoUrl.searchParams.set('count', '1');
geoUrl.searchParams.set('language', 'en');

const geoRes = await fetch(geoUrl.toString());
if (!geoRes.ok) return null;
const geoData = await geoRes.json();
if (!geoData || !geoData.results || !geoData.results.length) return null;

const loc = geoData.results[0];
const weatherUrl = new URL('https://api.open-meteo.com/v1/forecast');
weatherUrl.searchParams.set('latitude', loc.latitude);
weatherUrl.searchParams.set('longitude', loc.longitude);
weatherUrl.searchParams.set('current_weather', 'true');
weatherUrl.searchParams.set('temperature_unit', tempUnit === 'F' ? 'fahrenheit' : 'celsius');

const weatherRes = await fetch(weatherUrl.toString());
if (!weatherRes.ok) return null;
const weatherData = await weatherRes.json();
if (!weatherData || !weatherData.current_weather) return null;

return {
city: loc.name,
country: loc.country_code || '',
temp: Math.round(weatherData.current_weather.temperature),
unit: tempUnit === 'F' ? '°F' : '°C',
code: weatherData.current_weather.weathercode
};
} catch (_err) {
return null;
}
}

function svgWeather(weather, mode = 'live') {
const theme = themeObj(state.theme);
const borderStyle = theme.border?`stroke:${theme.fg};stroke-width:2;`:'';
const borderStyle = theme.border ? `stroke:${theme.fg};stroke-width:2;` : '';
const isDetailed = state.weatherStyle === 'detailed';
const W = isDetailed ? 320 : 200;
const H = isDetailed ? 110 : 80;
const icon = '☀️';
const tempStr = `24°${state.tempUnit}`;
const city = state.city || 'Tokyo';
const title = mode === 'loading' ? 'LOADING WEATHER' : mode === 'error' ? 'WEATHER UNAVAILABLE' : 'CURRENT WEATHER';
const tempStr = weather ? `${weather.temp}${weather.unit}` : `--°${state.tempUnit}`;
const city = weather ? `${weather.city}${weather.country ? ', ' + weather.country.toUpperCase() : ''}` : (state.city || 'Enter a city');

if (isDetailed) {
return `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="font-family:'JetBrains Mono',monospace;">
<rect x="1" y="1" width="${W-2}" height="${H-2}" rx="${state.radius||0}" ry="${state.radius||0}" fill="${theme.bg}" style="${borderStyle}"/>
<text x="20" y="32" fill="${theme.fg}" font-size="10" font-weight="700" letter-spacing="2" opacity="0.65">CURRENT WEATHER</text>
<text x="20" y="32" fill="${theme.fg}" font-size="10" font-weight="700" letter-spacing="2" opacity="0.65">${title}</text>
<text x="20" y="80" font-size="42">${icon}</text>
<text x="80" y="65" fill="${theme.fg}" font-size="28" font-weight="800" font-family="Fraunces,serif">${tempStr}</text>
<text x="80" y="85" fill="${theme.fg}" font-size="12" font-weight="600" opacity="0.85">${city}</text>
Expand All @@ -2930,6 +2982,7 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
}
}


function svgYoutube(){
const theme = themeObj(state.theme);
const borderStyle = theme.border?`stroke:${theme.fg};stroke-width:2;`:'';
Expand Down Expand Up @@ -3096,6 +3149,7 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
`;
}


function svgContributors() {
const theme = themeObj(state.theme);
const rx = state.radius !== undefined ? state.radius : 8;
Expand Down Expand Up @@ -3363,8 +3417,37 @@ <h4 class="font-serif font-black text-3xl leading-none">Stylish Readme</h4>
}

function renderPreview(){
$('previewStage').innerHTML = getSvg();
const size = (new Blob([getSvg()]).size/1024).toFixed(1);
const stage = $('previewStage');
if (!stage) return;

if (state.widget === 'weather') {
const key = [state.city, state.tempUnit, state.weatherStyle, state.theme, state.radius || 0].join('|');
const alreadyRendered = currentPreviewWidget === 'weather' && stage.dataset.weatherKey === key;
currentPreviewWidget = 'weather';
stage.dataset.weatherKey = key;

if (alreadyRendered) return;

const seq = ++weatherPreviewSeq;
weatherPreviewKey = key;
const loadingSvg = svgWeather(null, 'loading');
stage.innerHTML = loadingSvg;
$('sizeInfo').textContent = (new Blob([loadingSvg]).size/1024).toFixed(1);

fetchWeatherPreview(state.city, state.tempUnit).then(weather=>{
if (seq !== weatherPreviewSeq || state.widget !== 'weather' || weatherPreviewKey !== key) return;
const svg = weather ? svgWeather(weather, 'live') : svgWeather(null, 'error');
stage.innerHTML = svg;
$('sizeInfo').textContent = (new Blob([svg]).size/1024).toFixed(1);
});
return;
}

currentPreviewWidget = state.widget;
stage.dataset.weatherKey = '';
const svg = getSvg();
stage.innerHTML = svg;
const size = (new Blob([svg]).size/1024).toFixed(1);
$('sizeInfo').textContent = size;
}

Expand Down
15 changes: 9 additions & 6 deletions lib/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -2370,6 +2370,7 @@ function fetchJson(urlObj) {
});
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
req.setTimeout(6500);
});
}

Expand Down Expand Up @@ -2397,18 +2398,18 @@ async function fetchWeather(city, tempUnit) {
const weatherUrl = new URL('https://api.open-meteo.com/v1/forecast');
weatherUrl.searchParams.set('latitude', loc.latitude.toString());
weatherUrl.searchParams.set('longitude', loc.longitude.toString());
weatherUrl.searchParams.set('current', 'temperature_2m,weather_code');
weatherUrl.searchParams.set('current_weather', 'true');
weatherUrl.searchParams.set('temperature_unit', tempUnit === 'F' ? 'fahrenheit' : 'celsius');

const weatherData = await fetchJson(weatherUrl);
if (!weatherData || !weatherData.current) return null;
if (!weatherData || !weatherData.current_weather) return null;

return {
city: loc.name,
country: loc.country_code || '',
temp: Math.round(weatherData.current.temperature_2m),
temp: Math.round(weatherData.current_weather.temperature),
unit: tempUnit === 'F' ? '°F' : '°C',
code: weatherData.current.weather_code
code: weatherData.current_weather.weathercode
};
}

Expand Down Expand Up @@ -2439,9 +2440,11 @@ async function renderWeather(p) {
const H = isDetailed ? 110 : 80;

if (!weather) {
const cityLabel = p.city ? ` for ${p.city}` : '';
return svgWrap(W, H, `
<rect x="1" y="1" width="${W-2}" height="${H-2}" rx="${rx}" ry="${rx}" fill="${bgFill}" ${border}/>
<text x="${W/2}" y="${H/2}" text-anchor="middle" fill="${th.fg}" font-size="12">City not found</text>
<text x="${W/2}" y="${H/2 - 6}" text-anchor="middle" fill="${th.fg}" font-size="12" font-weight="700">Weather unavailable</text>
<text x="${W/2}" y="${H/2 + 14}" text-anchor="middle" fill="${th.fg}" font-size="10" opacity="0.7">${escXml(cityLabel)}</text>
`);
}

Expand Down Expand Up @@ -3242,4 +3245,4 @@ module.exports = {
normalizeBool,
THEMES, TIMEZONES, COUNTRIES, FLAGS, SKILLS, CODING_PLATFORMS, MUSIC_PLATFORMS,
flagSvg, skillIcon, platformBadge
};
};
16 changes: 10 additions & 6 deletions vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,23 @@
{
"source": "/api/(time|clock|timezone|skyline).svg",
"headers": [

{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
{ "key": "Cache-Control", "value": "public, max-age=60, s-maxage=60, stale-while-revalidate=30" },

{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
{ "key": "Cache-Control", "value": "public, max-age=60, s-maxage=60, stale-while-revalidate=30" },

{ "key": "Vary", "value": "Accept-Encoding" },
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
},
{
"source": "/api/(date|quote|word|streak|profile).svg",
"source": "/api/weather.svg",
"headers": [
{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
{ "key": "Cache-Control", "value": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=600" },
{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
{ "key": "Cache-Control", "value": "public, max-age=60, s-maxage=60, stale-while-revalidate=30" },
{ "key": "Vary", "value": "Accept-Encoding" },
{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
{ "key": "Cache-Control", "value": "public, max-age=1800, s-maxage=1800, stale-while-revalidate=600" },
{ "key": "Vary", "value": "Accept-Encoding" },
{ "key": "Access-Control-Allow-Origin", "value": "*" }
]
},
Expand All @@ -38,6 +41,7 @@
]
},
{
"source": "/api/(date|quote|word|streak|profile|countdown|marker|glass|marketplace).svg",
"source": "/api/(date|quote|streak|profile|countdown|marker|glass|marketplace|progress).svg",
"headers": [
{ "key": "Content-Type", "value": "image/svg+xml; charset=utf-8" },
Expand Down