diff --git a/apps/marketing/next.config.js b/apps/marketing/next.config.js index 3b86bfe..a50b316 100644 --- a/apps/marketing/next.config.js +++ b/apps/marketing/next.config.js @@ -1,5 +1,7 @@ /** @type {import("next").NextConfig} */ const config = { + // Use static export in production by default; keep dev server dynamic + output: "export", images: { // Required for static export if using images unoptimized: true, diff --git a/apps/marketing/src/app/api/status/route.ts b/apps/marketing/src/app/api/status/route.ts deleted file mode 100644 index 904211d..0000000 --- a/apps/marketing/src/app/api/status/route.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { NextRequest } from "next/server"; - -export const dynamic = "force-dynamic"; - -type StatusState = "operational" | "degraded" | "down" | "unknown"; - -function normalizeStatus(data: any): StatusState { - const overall = (data?.overallStatus || data?.status || "") - .toString() - .toLowerCase(); - if ( - overall.includes("up") || - overall.includes("ok") || - overall.includes("oper") - ) - return "operational"; - if (overall.includes("degrad") || overall.includes("partial")) - return "degraded"; - if ( - overall.includes("down") || - overall.includes("outage") || - overall.includes("incident") - ) - return "down"; - - // Status page incidents (default endpoint) - const sp = (data?.statusPage || data?.status_page || {}) as any; - const incidentRaw = sp?.incident ?? sp?.incidents ?? data?.incident ?? data?.incidents; - if (incidentRaw) { - const incidents: any[] = Array.isArray(incidentRaw) - ? incidentRaw - : [incidentRaw]; - const hasActive = incidents.some((i) => { - const resolved = - i?.resolved === true || - (typeof i?.status === "string" && i.status.toLowerCase().includes("resolv")) || - !!i?.endAt || - !!i?.end_at || - !!i?.endTime || - !!i?.end_time; - return !resolved; - }); - if (hasActive) return "down"; - } - - // Heartbeat map shape: { [monitorId: string]: Heartbeat[] } - if (data?.heartbeatList && typeof data.heartbeatList === "object") { - const lists = Object.values(data.heartbeatList); - const isAnyDown = lists.some( - (arr: any) => Array.isArray(arr) && arr.some((hb) => hb?.status === 0) - ); - if (isAnyDown) return "down"; - return "operational"; - } - - if (typeof data?.allUp === "boolean") - return data.allUp ? "operational" : "down"; - - return "unknown"; -} - -export async function GET(_req: NextRequest) { - const base = ( - process.env.STATUS_BASE_URL || "https://status.usesend.com" - ).replace(/\/$/, ""); - const candidates = [ - "/api/status-page/default", - "/api/status-page/heartbeat/default", - ]; - let status = "unknown"; - - for (const path of candidates) { - try { - const res = await fetch(`${base}${path}`, { cache: "no-store" }); - if (!res.ok) continue; - const data = await res.json().catch(() => null); - if (!data) continue; - status = normalizeStatus(data); - if (status !== "unknown") break; - } catch { - // try next candidate - continue; - } - } - - return Response.json( - { status }, - { - headers: { "Cache-Control": "s-maxage=30, stale-while-revalidate=300" }, - } - ); -} diff --git a/apps/marketing/src/components/SiteFooter.tsx b/apps/marketing/src/components/SiteFooter.tsx index 1de5b50..43c6c80 100644 --- a/apps/marketing/src/components/SiteFooter.tsx +++ b/apps/marketing/src/components/SiteFooter.tsx @@ -1,6 +1,6 @@ import Image from "next/image"; import Link from "next/link"; -import { StatusBadge } from "~/components/StatusBadge"; +// Replaced StatusBadge with external status badge image const REPO = "usesend/usesend"; const REPO_URL = `https://github.com/${REPO}`; @@ -132,7 +132,20 @@ export function SiteFooter() { - + + Service status + diff --git a/apps/marketing/src/components/StatusBadge.tsx b/apps/marketing/src/components/StatusBadge.tsx deleted file mode 100644 index 1f8e114..0000000 --- a/apps/marketing/src/components/StatusBadge.tsx +++ /dev/null @@ -1,86 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; - -type StatusState = "operational" | "degraded" | "down" | "unknown"; - -// Fetch normalized status from the app's server API to bypass CORS. -async function fetchUptimeStatus(): Promise { - try { - const res = await fetch("/api/status", { cache: "no-store" }); - if (!res.ok) return "unknown"; - const data: any = await res.json().catch(() => null); - const s = (data?.status || "").toString().toLowerCase(); - console.log(data); - if (s === "operational" || s === "degraded" || s === "down") - return s as StatusState; - return "unknown"; - } catch { - return "unknown"; - } -} - -export function StatusBadge({ - baseUrl = "https://status.usesend.com", -}: { - baseUrl?: string; -}) { - const [status, setStatus] = useState("unknown"); - - useEffect(() => { - let mounted = true; - const load = async () => { - const s = await fetchUptimeStatus(); - - if (mounted) setStatus(s); - }; - load(); - const id = setInterval(load, 60_000); // refresh every 60s - return () => { - mounted = false; - clearInterval(id); - }; - }, []); - - const dotClass = useMemo(() => { - switch (status) { - case "operational": - return "bg-green"; - case "degraded": - return "bg-yellow"; - case "down": - return "bg-red"; - default: - return "bg-muted-foreground"; - } - }, [status]); - - const label = useMemo(() => { - switch (status) { - case "operational": - return "operational"; - case "degraded": - return "degraded"; - case "down": - return "outage"; - default: - return "unknown"; - } - }, [status]); - - return ( - - - {label} - - ); -}