diff --git a/app/api/version/route.ts b/app/api/version/route.ts new file mode 100644 index 0000000..6c133d4 --- /dev/null +++ b/app/api/version/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server' + +// BUILD_ID is set at build time by Next.js standalone output. +// Falls back to a startup timestamp so every restart counts as a new version. +const VERSION = process.env.BUILD_ID || process.env.NEXT_BUILD_ID || Date.now().toString() + +export function GET() { + return NextResponse.json( + { version: VERSION }, + { + headers: { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }, + } + ) +} diff --git a/app/layout.tsx b/app/layout.tsx index 7e183df..60c3fab 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata } from "next" import { Geist, Geist_Mono, Fredoka, Permanent_Marker } from "next/font/google" import { MusicProvider } from "@/components/music/music-provider" import { MiniPlayer } from "@/components/music/mini-player" +import { UpdateBanner } from "@/components/update-banner" import "./globals.css" const _geist = Geist({ subsets: ["latin"] }) @@ -72,6 +73,7 @@ export default function RootLayout({ return ( + {children} diff --git a/components/update-banner.tsx b/components/update-banner.tsx new file mode 100644 index 0000000..d9ac9a5 --- /dev/null +++ b/components/update-banner.tsx @@ -0,0 +1,54 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { RefreshCw } from 'lucide-react' + +const CHECK_INTERVAL = 60_000 // check every 60s + +export function UpdateBanner() { + const [updateAvailable, setUpdateAvailable] = useState(false) + const [initialVersion, setInitialVersion] = useState(null) + + const checkVersion = useCallback(async () => { + try { + const res = await fetch('/api/version', { cache: 'no-store' }) + const { version } = await res.json() + if (!version) return + + if (initialVersion === null) { + setInitialVersion(version) + } else if (version !== initialVersion) { + setUpdateAvailable(true) + } + } catch { + // network error, skip + } + }, [initialVersion]) + + useEffect(() => { + checkVersion() + const id = setInterval(checkVersion, CHECK_INTERVAL) + return () => clearInterval(id) + }, [checkVersion]) + + // Also check on visibility change (user returns to tab) + useEffect(() => { + const onVisible = () => { + if (document.visibilityState === 'visible') checkVersion() + } + document.addEventListener('visibilitychange', onVisible) + return () => document.removeEventListener('visibilitychange', onVisible) + }, [checkVersion]) + + if (!updateAvailable) return null + + return ( + + ) +} diff --git a/next.config.mjs b/next.config.mjs index fabed9b..6280284 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -7,6 +7,9 @@ const nextConfig = { unoptimized: true, }, output: 'standalone', + env: { + BUILD_ID: process.env.BUILD_ID || new Date().toISOString(), + }, async redirects() { return [ {