Skip to content

Commit ec605e2

Browse files
committed
feat(site): add offline-safe GitHub signals section
1 parent 61932e8 commit ec605e2

File tree

9 files changed

+310
-28
lines changed

9 files changed

+310
-28
lines changed

vix-site/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
"version": "0.0.0",
55
"type": "module",
66
"scripts": {
7+
"predev": "node scripts/fetch_github_stats.mjs",
78
"dev": "vite",
9+
"prebuild": "node scripts/fetch_github_stats.mjs",
810
"build": "vite build",
911
"deploy": "npm run build && node scripts/deploy.js",
1012
"preview": "vite preview"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import fs from "node:fs";
2+
import path from "node:path";
3+
4+
const OUT = path.resolve("src/data/github_stats.json");
5+
const REPO = "vixcpp/vix";
6+
const API = `https://api.github.com/repos/${REPO}`;
7+
8+
function nowISO() {
9+
return new Date().toISOString();
10+
}
11+
12+
async function fetchJson(url, headers) {
13+
const res = await fetch(url, { headers });
14+
if (!res.ok)
15+
throw new Error(`GitHub API failed: ${res.status} ${res.statusText}`);
16+
return res.json();
17+
}
18+
19+
async function main() {
20+
const token = process.env.GITHUB_TOKEN || "";
21+
const headers = {
22+
Accept: "application/vnd.github+json",
23+
"User-Agent": "vix-site-build",
24+
};
25+
26+
if (token) headers["Authorization"] = `Bearer ${token}`;
27+
28+
let payload;
29+
try {
30+
const repo = await fetchJson(API, headers);
31+
32+
payload = {
33+
repo: REPO,
34+
fetched_at: nowISO(),
35+
stars: repo.stargazers_count ?? 0,
36+
forks: repo.forks_count ?? 0,
37+
open_issues: repo.open_issues_count ?? 0,
38+
watchers: repo.subscribers_count ?? 0,
39+
};
40+
} catch (err) {
41+
// Fallback: si le build n'a pas accès au net, on conserve l'ancien fichier si présent
42+
if (fs.existsSync(OUT)) {
43+
console.warn(
44+
`[github_stats] fetch failed, keeping existing file: ${err.message}`,
45+
);
46+
return;
47+
}
48+
49+
// Dernier recours: fichier minimal
50+
console.warn(
51+
`[github_stats] fetch failed, writing fallback file: ${err.message}`,
52+
);
53+
payload = {
54+
repo: REPO,
55+
fetched_at: nowISO(),
56+
stars: 0,
57+
forks: 0,
58+
open_issues: 0,
59+
watchers: 0,
60+
fallback: true,
61+
};
62+
}
63+
64+
fs.mkdirSync(path.dirname(OUT), { recursive: true });
65+
fs.writeFileSync(OUT, JSON.stringify(payload, null, 2), "utf8");
66+
console.log(`[github_stats] wrote ${OUT}`);
67+
}
68+
69+
main().catch((e) => {
70+
console.error(e);
71+
process.exit(1);
72+
});

vix-site/src/components/CodeBlock.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,16 @@ function normalizeShellText(raw) {
145145
const s = String(raw ?? "");
146146
return s
147147
.split("\n")
148-
.map((line) => line.replace(/^\s*>\s?/, ""))
148+
.map((line) => {
149+
// 1) remove markdown quote at line start
150+
let out = line.replace(/^\s*>\s?/, "");
151+
152+
// 2) remove weird " >:8080" prompt chunk (">:" with optional spaces)
153+
// turns "http >:8080/api/ping" into "http :8080/api/ping"
154+
out = out.replace(/\s*>\s*:(\d{2,5})/g, " :$1");
155+
156+
return out;
157+
})
149158
.join("\n");
150159
}
151160
@@ -333,7 +342,7 @@ function highlightShell(raw) {
333342
s = s.replace(/(https?:\/\/[^\s]+)/g, `<span class="shell-url">$1</span>`);
334343
s = s.replace(/(\s(?:\.{0,2}\/[^\s]+))/g, `<span class="shell-path">$1</span>`);
335344
s = s.replace(/(:\d{2,5}\b)/g, `<span class="shell-port">$1</span>`);
336-
s = s.replace(/(\s\|\s|\s\|\|\s|>\s*[^\s]+)/g, `<span class="shell-op">$1</span>`);
345+
s = s.replace(/(\s\|\|\s|\s\|\s)/g, `<span class="shell-op">$1</span>`);
337346
s = s.replace(/^(HTTP\/\d\.\d\s+\d+\s+.*)$/gm, `<span class="shell-http">$1</span>`);
338347
s = s.replace(/^([A-Za-z-]+:\s*)(.*)$/gm, `<span class="shell-hdr">$1</span><span class="shell-hdrv">$2</span>`);
339348
@@ -389,6 +398,11 @@ async function copy(text) {
389398
overflow: hidden;
390399
box-shadow: 0 18px 46px rgba(0,0,0,.55);
391400
}
401+
.code-card{
402+
width: 100%;
403+
max-width: 100%;
404+
margin: 0;
405+
}
392406
393407
@media (max-width: 720px){
394408
.code-card{
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<script setup>
2+
const props = defineProps({
3+
items: { type: Array, default: () => [] },
4+
github: { type: Object, default: null },
5+
});
6+
7+
function formatNumber(n) {
8+
const x = Number(n);
9+
if (!Number.isFinite(x)) return "";
10+
return x.toLocaleString();
11+
}
12+
</script>
13+
14+
<template>
15+
<div class="signals-grid">
16+
<div v-for="item in items" :key="item.title" class="signal-card">
17+
<div class="signal-title">{{ item.title }}</div>
18+
19+
<div class="signal-value">
20+
<template v-if="item.kind === 'github' && github">
21+
{{ formatNumber(github[item.field]) }}
22+
</template>
23+
<template v-else>
24+
{{ item.value }}
25+
</template>
26+
</div>
27+
28+
<ul v-if="item.meta?.length" class="signal-meta">
29+
<li v-for="m in item.meta" :key="m">{{ m }}</li>
30+
</ul>
31+
</div>
32+
</div>
33+
</template>
34+
35+
<style scoped>
36+
.signals-grid {
37+
display: grid;
38+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
39+
gap: 1.2rem;
40+
}
41+
42+
.signal-card {
43+
padding: 1.2rem;
44+
border-radius: 16px;
45+
background: rgba(255, 255, 255, 0.04);
46+
border: 1px solid rgba(255, 255, 255, 0.08);
47+
}
48+
49+
.signal-title {
50+
font-size: 0.85rem;
51+
opacity: 0.7;
52+
}
53+
54+
.signal-value {
55+
margin-top: 0.35rem;
56+
font-size: 1.2rem;
57+
font-weight: 650;
58+
letter-spacing: -0.01em;
59+
}
60+
61+
.signal-meta {
62+
margin-top: 0.8rem;
63+
padding: 0;
64+
list-style: none;
65+
display: grid;
66+
gap: 0.35rem;
67+
font-size: 0.9rem;
68+
opacity: 0.85;
69+
}
70+
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"repo": "vixcpp/vix",
3+
"fetched_at": "2026-02-08T09:26:34.572Z",
4+
"stars": 0,
5+
"forks": 0,
6+
"open_issues": 0,
7+
"watchers": 0,
8+
"fallback": true
9+
}

vix-site/src/data/home.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,31 @@ i Hint: Ctrl+C to stop`,
108108
],
109109
},
110110

111+
signals: {
112+
title: "Project signals",
113+
subtitle: "Live GitHub stats with safe fallback.",
114+
items: [
115+
{
116+
title: "Stars",
117+
kind: "github",
118+
field: "stars",
119+
meta: ["GitHub stars"],
120+
},
121+
{
122+
title: "Forks",
123+
kind: "github",
124+
field: "forks",
125+
meta: ["Community forks"],
126+
},
127+
{
128+
title: "Open issues",
129+
kind: "github",
130+
field: "open_issues",
131+
meta: ["Work in progress"],
132+
},
133+
],
134+
},
135+
111136
batteries: {
112137
title: "What ships with Vix",
113138
items: [

vix-site/src/lib/githubStats.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
const REPO = "vixcpp/vix";
2+
const API = `https://api.github.com/repos/${REPO}`;
3+
4+
const LS_KEY = `vix_github_stats_${REPO}`;
5+
const TTL_MS = 6 * 60 * 60 * 1000;
6+
7+
function isFresh(ts) {
8+
if (!ts) return false;
9+
const t = new Date(ts).getTime();
10+
return Number.isFinite(t) && Date.now() - t < TTL_MS;
11+
}
12+
13+
function readCache() {
14+
try {
15+
const raw = localStorage.getItem(LS_KEY);
16+
if (!raw) return null;
17+
const data = JSON.parse(raw);
18+
if (!isFresh(data?.fetched_at)) return null;
19+
return data;
20+
} catch {
21+
return null;
22+
}
23+
}
24+
25+
function writeCache(data) {
26+
try {
27+
localStorage.setItem(LS_KEY, JSON.stringify(data));
28+
} catch {}
29+
}
30+
31+
// ✅ no Vite import resolution, no crash
32+
async function loadBuildStats() {
33+
try {
34+
const url = new URL("../data/github_stats.json", import.meta.url);
35+
const res = await fetch(url);
36+
if (!res.ok) throw new Error("stats json missing");
37+
return await res.json();
38+
} catch {
39+
return {
40+
repo: REPO,
41+
fetched_at: null,
42+
stars: 0,
43+
forks: 0,
44+
open_issues: 0,
45+
watchers: 0,
46+
fallback: true,
47+
};
48+
}
49+
}
50+
51+
export async function getInitialGithubStats() {
52+
return readCache() || (await loadBuildStats());
53+
}
54+
55+
export async function refreshGithubStats({ timeoutMs = 1200 } = {}) {
56+
const controller = new AbortController();
57+
const t = setTimeout(() => controller.abort(), timeoutMs);
58+
59+
try {
60+
const res = await fetch(API, {
61+
signal: controller.signal,
62+
headers: { Accept: "application/vnd.github+json" },
63+
});
64+
65+
if (!res.ok) return null;
66+
67+
const repo = await res.json();
68+
69+
const data = {
70+
repo: REPO,
71+
fetched_at: new Date().toISOString(),
72+
stars: repo.stargazers_count ?? 0,
73+
forks: repo.forks_count ?? 0,
74+
open_issues: repo.open_issues_count ?? 0,
75+
watchers: repo.subscribers_count ?? 0,
76+
};
77+
78+
writeCache(data);
79+
return data;
80+
} catch {
81+
return null;
82+
} finally {
83+
clearTimeout(t);
84+
}
85+
}

0 commit comments

Comments
 (0)