import { useEffect, useState } from "react"; import { Braces } from "lucide-react"; import { useTranslation } from "react-i18next"; import { registerWidget } from "./WidgetRegistry"; import type { CustomApiConfig, WidgetComponentProps, } from "@/types/homepage-types"; import { GRID_SIZE } from "@/types/homepage-types"; import { homepageApi } from "@/main-axios"; import { WidgetTitle } from "./WidgetTitle"; import { runVisibleInterval } from "../use-visible-interval"; function resolvePath(obj: unknown, path: string): unknown { return path.split(".").reduce((acc: unknown, key) => { if (acc && typeof acc === "object") return (acc as Record)[key]; return undefined; }, obj); } function resolveArray( obj: unknown, path: string | undefined, ): unknown[] | null { if (!path) { if (Array.isArray(obj)) return obj as unknown[]; return null; } const val = resolvePath(obj, path); return Array.isArray(val) ? (val as unknown[]) : null; } async function fetchProxy(url: string, ttl: number): Promise { const res = await homepageApi.get("/proxy", { params: { url, ttl } }); return res.data; } function CustomApiWidget({ widget, config, }: WidgetComponentProps) { const { t } = useTranslation(); const { url, displayField, label, unit, refreshInterval, displayMode, jsonPath, } = config; const [data, setData] = useState(null); const [error, setError] = useState(false); const [loading, setLoading] = useState(true); const interval = Math.max(10, refreshInterval || 60); const fetchData = async () => { if (!url) return; try { const result = await fetchProxy(url, interval); setData(result); setError(false); } catch { setError(true); } finally { setLoading(false); } }; useEffect(() => { fetchData(); return runVisibleInterval(() => { void fetchData(); }, interval * 1000); }, [url, interval]); const accent = getComputedStyle(document.documentElement) .getPropertyValue("--accent-brand") .trim() || "#f59145"; if (!url) { return (
{t("homepage.customApiNoUrl")}
); } if (loading) { return (
{t("homepage.loading")}
); } if (error) { return (
{t("homepage.customApiError")}
); } const titleBar = ( } /> ); if (displayMode === "value") { const val = displayField ? resolvePath(data, displayField) : data; const display = typeof val === "object" ? JSON.stringify(val) : String(val ?? "—"); return (
{titleBar}
{label && ( {label} )} {display} {unit && ( {unit} )}
); } if (displayMode === "table") { const arr = resolveArray(data, jsonPath); if (!arr) { return (
{t("homepage.customApiNotArray")}
); } return (
{titleBar}
{arr.slice(0, 50).map((row, i) => { if (typeof row !== "object" || row === null) { return ( ); } return ( {Object.entries(row as Record) .slice(0, 4) .map(([k, v]) => ( ))} ); })}
{String(row)}
{k}: {String(v ?? "")}
); } // json mode return (
{titleBar}
          {JSON.stringify(data, null, 2)}
        
); } registerWidget({ id: "custom_api", name: "Custom API", description: "Fetch and display data from any JSON API", category: "info", icon: , defaultConfig: { url: "", refreshInterval: 60, displayMode: "value" }, defaultSize: { w: GRID_SIZE * 10, h: GRID_SIZE * 7 }, minSize: { w: GRID_SIZE * 4, h: GRID_SIZE * 3 }, component: CustomApiWidget, }); export { CustomApiWidget };