mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
docs: dynamically fetch latest version from GitHub API
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "开启自由之旅",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user