docs: dynamically fetch latest version from GitHub API

This commit is contained in:
weishu
2026-01-02 13:30:56 +08:00
parent 8cacd04db4
commit db6b227122
4 changed files with 63 additions and 4 deletions
+57
View File
@@ -0,0 +1,57 @@
import { useState, useEffect } from "react";
const GITHUB_REPO = "tiann/hapi";
const CACHE_KEY = "hapi-latest-version";
const CACHE_TTL = 1000 * 60 * 60; // 1 hour
interface CachedVersion {
version: string;
timestamp: number;
}
export function useLatestVersion(fallback: string = "latest") {
const [version, setVersion] = useState<string>(() => {
// Try to get from cache first
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const { version, timestamp }: CachedVersion = JSON.parse(cached);
if (Date.now() - timestamp < CACHE_TTL) {
return version;
}
}
} catch {
// Ignore cache errors
}
return fallback;
});
useEffect(() => {
const fetchVersion = async () => {
try {
const res = await fetch(
`https://api.github.com/repos/${GITHUB_REPO}/releases/latest`,
{ headers: { Accept: "application/vnd.github.v3+json" } }
);
if (!res.ok) return;
const data = await res.json();
const tag = data.tag_name as string;
if (tag) {
setVersion(tag);
// Cache the result
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ version: tag, timestamp: Date.now() })
);
}
} catch {
// Keep fallback on error
}
};
fetchVersion();
}, []);
return version;
}
+1 -1
View File
@@ -7,7 +7,7 @@
"viewOnGithub": "View on GitHub" "viewOnGithub": "View on GitHub"
}, },
"hero": { "hero": {
"version": "v0.3.1 is now available", "version": "{{version}} is now available",
"title": "Vibe Coding <br/> <span class=\"text-primary\">Anytime, Anywhere.</span>", "title": "Vibe Coding <br/> <span class=\"text-primary\">Anytime, Anywhere.</span>",
"subtitle": "Go for a hike, grab a coffee, or just relax. Your AI agents work in the background. We'll notify you via your favorite messaging app only when they need a nod.", "subtitle": "Go for a hike, grab a coffee, or just relax. Your AI agents work in the background. We'll notify you via your favorite messaging app only when they need a nod.",
"startBtn": "Start Your Freedom", "startBtn": "Start Your Freedom",
+1 -1
View File
@@ -7,7 +7,7 @@
"viewOnGithub": "GitHub" "viewOnGithub": "GitHub"
}, },
"hero": { "hero": {
"version": "v0.3.1 现已发布", "version": "{{version}} 现已发布",
"title": "Vibe Coding <br/> <span class=\"text-primary\">随时随地,自由编程</span>", "title": "Vibe Coding <br/> <span class=\"text-primary\">随时随地,自由编程</span>",
"subtitle": "去徒步,去喝咖啡,或者只是放松一下。你的 AI 代理在后台为你工作。只有在需要你确认时,我们才会通过你喜欢的即时通讯应用通知你。", "subtitle": "去徒步,去喝咖啡,或者只是放松一下。你的 AI 代理在后台为你工作。只有在需要你确认时,我们才会通过你喜欢的即时通讯应用通知你。",
"startBtn": "开启自由之旅", "startBtn": "开启自由之旅",
+4 -2
View File
@@ -9,11 +9,13 @@ import { Link } from "wouter";
import { motion, useScroll, useTransform } from "framer-motion"; import { motion, useScroll, useTransform } from "framer-motion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SEO } from "@/components/SEO"; import { SEO } from "@/components/SEO";
import { useLatestVersion } from "@/hooks/useLatestVersion";
export default function Home() { export default function Home() {
const [copied, setCopied] = useState(""); const [copied, setCopied] = useState("");
const { scrollY } = useScroll(); const { scrollY } = useScroll();
const { t } = useTranslation(); const { t } = useTranslation();
const version = useLatestVersion();
const copyToClipboard = (text: string, key: string) => { const copyToClipboard = (text: string, key: string) => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
@@ -34,9 +36,9 @@ export default function Home() {
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span> <span className="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
</span> </span>
{t('hero.version')} {t('hero.version', { version })}
</div> </div>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold tracking-tight leading-[1.1] mb-6" dangerouslySetInnerHTML={{ __html: t('hero.title') }} /> <h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold tracking-tight leading-[1.1] mb-6" dangerouslySetInnerHTML={{ __html: t('hero.title') }} />
<p className="text-xl text-muted-foreground max-w-lg leading-relaxed"> <p className="text-xl text-muted-foreground max-w-lg leading-relaxed">