fix marketing page build
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
|
// Use static export in production by default; keep dev server dynamic
|
||||||
|
output: "export",
|
||||||
images: {
|
images: {
|
||||||
// Required for static export if using images
|
// Required for static export if using images
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
|
@@ -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<any>(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" },
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { StatusBadge } from "~/components/StatusBadge";
|
// Replaced StatusBadge with external status badge image
|
||||||
|
|
||||||
const REPO = "usesend/usesend";
|
const REPO = "usesend/usesend";
|
||||||
const REPO_URL = `https://github.com/${REPO}`;
|
const REPO_URL = `https://github.com/${REPO}`;
|
||||||
@@ -132,7 +132,20 @@ export function SiteFooter() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusBadge />
|
<a
|
||||||
|
href="https://status.usesend.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label="Service status"
|
||||||
|
title="Service status"
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://status.usesend.com/api/badge/2/status?upColor=30D9BD&style=plastic"
|
||||||
|
alt="Service status"
|
||||||
|
className="h-5"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -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<StatusState> {
|
|
||||||
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<StatusState>("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 (
|
|
||||||
<a
|
|
||||||
href={baseUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-primary/30 bg-primary/10 px-3 py-1 text-xs font-medium text-primary hover:bg-primary/20 transition-colors"
|
|
||||||
aria-label={`Service status: ${label}`}
|
|
||||||
title={`Status: ${label}`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`inline-block h-2 w-2 rounded-full ${dotClass} shadow-[0_0_0_2px] shadow-background`}
|
|
||||||
/>
|
|
||||||
{label}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
Reference in New Issue
Block a user