"use client"; import { useQuery_experimental } from "convex/react"; import { useState, useMemo, type CSSProperties, type JSX } from "react"; import { makeFunctionReference, type FunctionReference } from "convex/server"; type DeploymentInfo = { _id: string; _creationTime: number; currentDeploymentId: string; deployedAt: number; spaFallback?: boolean; } | null; type DeploymentQueryRef = FunctionReference< "query", "public", Record, DeploymentInfo >; const DEFAULT_QUERY_REF = makeFunctionReference< "query", Record, DeploymentInfo >("staticHosting:getCurrentDeployment") as DeploymentQueryRef; /* eslint-disable react-refresh/only-export-components */ /** * Hook to detect when a new deployment is available. * * @param getCurrentDeployment - Optional query reference. Defaults to * `api.staticHosting.getCurrentDeployment` (resolved by path). If you've * re-exported the deployment query under a different module name, pass the * reference explicitly. * * @example * ```tsx * import { useDeploymentUpdates } from "@convex-dev/static-hosting/react"; * * function App() { * const { updateAvailable, reload } = useDeploymentUpdates(); * return updateAvailable ? : null; * } * ``` */ export function useDeploymentUpdates( getCurrentDeployment?: DeploymentQueryRef, ) { const result = useQuery_experimental({ query: getCurrentDeployment ?? DEFAULT_QUERY_REF, args: {}, }); const deployment = result.status === "success" ? result.data : null; const setupError = result.status === "error" ? deploymentQuerySetupHelp(getCurrentDeployment) : null; const [initialDeploymentId, setInitialDeploymentId] = useState( null, ); const [dismissedDeploymentId, setDismissedDeploymentId] = useState< string | null >(null); if (deployment && initialDeploymentId === null) { setInitialDeploymentId(deployment.currentDeploymentId); } const updateAvailable = useMemo(() => { if (!deployment || initialDeploymentId === null) return false; const hasNew = deployment.currentDeploymentId !== initialDeploymentId; const isDismissed = deployment.currentDeploymentId === dismissedDeploymentId; return hasNew && !isDismissed; }, [deployment, initialDeploymentId, dismissedDeploymentId]); const reload = () => { window.location.reload(); }; const dismiss = () => { if (deployment) { setDismissedDeploymentId(deployment.currentDeploymentId); } }; return { updateAvailable, reload, dismiss, deployment, /** * When set, the deployment query isn't exported from the app; the banner * won't work until the user wires it up. The string is suitable to render * during local development so the missing setup is obvious. */ setupError, }; } function deploymentQuerySetupHelp(ref: DeploymentQueryRef | undefined): string { if (ref) { return "Deployment query failed. Verify the function you passed to useDeploymentUpdates is deployed."; } return [ "@convex-dev/static-hosting: UpdateBanner requires you to expose the deployment query.", "Create convex/staticHosting.ts:", "", ' import { exposeDeploymentQuery } from "@convex-dev/static-hosting";', ' import { components } from "./_generated/api";', " export const { getCurrentDeployment } = exposeDeploymentQuery(", " components.staticHosting,", " );", ].join("\n"); } /** * Ready-to-use banner shown when a new deployment is available. * * @example * ```tsx * import { UpdateBanner } from "@convex-dev/static-hosting/react"; * * ``` */ export function UpdateBanner({ getCurrentDeployment, message = "A new version is available!", buttonText = "Reload", dismissable = true, className, style, }: { getCurrentDeployment?: DeploymentQueryRef; message?: string; buttonText?: string; dismissable?: boolean; className?: string; style?: CSSProperties; } = {}): JSX.Element | null { const { updateAvailable, reload, dismiss, setupError } = useDeploymentUpdates(getCurrentDeployment); if (setupError) { if (typeof console !== "undefined") { console.warn(setupError); } return null; } if (!updateAvailable) return null; const defaultStyle: CSSProperties = { position: "fixed", bottom: "1rem", right: "1rem", backgroundColor: "#1a1a2e", color: "#fff", padding: "1rem 1.5rem", borderRadius: "8px", boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)", display: "flex", alignItems: "center", gap: "1rem", zIndex: 9999, fontFamily: "system-ui, -apple-system, sans-serif", fontSize: "14px", ...style, }; const buttonStyle: CSSProperties = { backgroundColor: "#4f46e5", color: "#fff", border: "none", padding: "0.5rem 1rem", borderRadius: "4px", cursor: "pointer", fontWeight: 500, }; const dismissStyle: CSSProperties = { background: "none", border: "none", color: "#888", cursor: "pointer", padding: "0.25rem", fontSize: "18px", lineHeight: 1, }; return (
{message} {dismissable && ( )}
); }