mirror of
https://github.com/gethomepage/homepage.git
synced 2026-04-02 16:22:14 -07:00
Feature: sparkyfitness service widget (#6346)
This commit is contained in:
15
docs/widgets/services/sparkyfitness.md
Normal file
15
docs/widgets/services/sparkyfitness.md
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: SparkyFitness
|
||||
description: SparkyFitness Widget Configuration
|
||||
---
|
||||
|
||||
Learn more about [SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness).
|
||||
|
||||
Allowed fields: `["eaten", "burned", "remaining", "steps"]`.
|
||||
|
||||
```yaml
|
||||
widget:
|
||||
type: sparkyfitness
|
||||
url: http://sparkyfitness.host.or.ip
|
||||
key: apikeyapikeyapikeyapikeyapikey
|
||||
```
|
||||
@@ -152,6 +152,7 @@ nav:
|
||||
- widgets/services/seerr.md
|
||||
- widgets/services/slskd.md
|
||||
- widgets/services/sonarr.md
|
||||
- widgets/services/sparkyfitness.md
|
||||
- widgets/services/speedtest-tracker.md
|
||||
- widgets/services/spoolman.md
|
||||
- widgets/services/stash.md
|
||||
|
||||
@@ -1174,5 +1174,11 @@
|
||||
"paused": "Paused",
|
||||
"total": "Total",
|
||||
"environment_not_found": "Environment Not Found"
|
||||
},
|
||||
"sparkyfitness": {
|
||||
"eaten": "Eaten",
|
||||
"burned": "Burned",
|
||||
"remaining": "Remaining",
|
||||
"steps": "Steps"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ const components = {
|
||||
seerr: dynamic(() => import("./seerr/component")),
|
||||
slskd: dynamic(() => import("./slskd/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
sparkyfitness: dynamic(() => import("./sparkyfitness/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
spoolman: dynamic(() => import("./spoolman/component")),
|
||||
stash: dynamic(() => import("./stash/component")),
|
||||
|
||||
35
src/widgets/sparkyfitness/component.jsx
Normal file
35
src/widgets/sparkyfitness/component.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Block from "components/services/widget/block";
|
||||
import Container from "components/services/widget/container";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||
|
||||
export default function Component({ service }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { widget } = service;
|
||||
const { data, error } = useWidgetAPI(widget, "stats");
|
||||
|
||||
if (error) {
|
||||
return <Container service={service} error={error} />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label="sparkyfitness.eaten" />
|
||||
<Block label="sparkyfitness.burned" />
|
||||
<Block label="sparkyfitness.remaining" />
|
||||
<Block label="sparkyfitness.steps" />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container service={service}>
|
||||
<Block label={t("sparkyfitness.eaten", "Eaten")} value={t("common.number", { value: data.eaten })} />
|
||||
<Block label={t("sparkyfitness.burned", "Burned")} value={t("common.number", { value: data.burned })} />
|
||||
<Block label={t("sparkyfitness.remaining", "Remaining")} value={t("common.number", { value: data.remaining })} />
|
||||
<Block label={t("sparkyfitness.steps", "Steps")} value={t("common.number", { value: data.steps })} />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
67
src/widgets/sparkyfitness/component.test.jsx
Normal file
67
src/widgets/sparkyfitness/component.test.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { renderWithProviders } from "test-utils/render-with-providers";
|
||||
import { findServiceBlockByLabel } from "test-utils/widget-assertions";
|
||||
|
||||
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
|
||||
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
|
||||
|
||||
import Component from "./component";
|
||||
|
||||
function expectBlockValue(container, label, value) {
|
||||
const block = findServiceBlockByLabel(container, label);
|
||||
expect(block, `missing block for ${label}`).toBeTruthy();
|
||||
expect(block.textContent).toContain(String(value));
|
||||
}
|
||||
|
||||
describe("widgets/sparkyfitness/component", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("calls the stats endpoint and renders placeholders while loading", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
|
||||
|
||||
const service = { widget: { type: "sparkyfitness", url: "http://x" } };
|
||||
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
|
||||
|
||||
expect(useWidgetAPI).toHaveBeenCalledWith(service.widget, "stats");
|
||||
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
|
||||
expect(screen.getByText("sparkyfitness.eaten")).toBeInTheDocument();
|
||||
expect(screen.getByText("sparkyfitness.burned")).toBeInTheDocument();
|
||||
expect(screen.getByText("sparkyfitness.remaining")).toBeInTheDocument();
|
||||
expect(screen.getByText("sparkyfitness.steps")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("-")).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("renders error UI when widget API errors", () => {
|
||||
useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } });
|
||||
|
||||
renderWithProviders(<Component service={{ widget: { type: "sparkyfitness", url: "http://x" } }} />, {
|
||||
settings: { hideErrors: false },
|
||||
});
|
||||
|
||||
expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("nope")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders numeric values when loaded", () => {
|
||||
useWidgetAPI.mockReturnValue({
|
||||
data: { eaten: 100, burned: 200, remaining: 300, steps: 400 },
|
||||
error: undefined,
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<Component service={{ widget: { type: "sparkyfitness", url: "http://x" } }} />,
|
||||
{ settings: { hideErrors: false } },
|
||||
);
|
||||
|
||||
expectBlockValue(container, "sparkyfitness.eaten", 100);
|
||||
expectBlockValue(container, "sparkyfitness.burned", 200);
|
||||
expectBlockValue(container, "sparkyfitness.remaining", 300);
|
||||
expectBlockValue(container, "sparkyfitness.steps", 400);
|
||||
});
|
||||
});
|
||||
15
src/widgets/sparkyfitness/widget.js
Normal file
15
src/widgets/sparkyfitness/widget.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
|
||||
const widget = {
|
||||
api: "{url}/{endpoint}",
|
||||
proxyHandler: credentialedProxyHandler,
|
||||
|
||||
mappings: {
|
||||
stats: {
|
||||
endpoint: "api/dashboard/stats",
|
||||
validate: ["eaten", "burned", "remaining", "steps"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default widget;
|
||||
15
src/widgets/sparkyfitness/widget.test.js
Normal file
15
src/widgets/sparkyfitness/widget.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { expectWidgetConfigShape } from "test-utils/widget-config";
|
||||
|
||||
import widget from "./widget";
|
||||
|
||||
describe("sparkyfitness widget config", () => {
|
||||
it("exports a valid widget config", () => {
|
||||
expectWidgetConfigShape(widget);
|
||||
|
||||
const statsMapping = widget.mappings?.stats;
|
||||
expect(statsMapping?.endpoint).toBe("api/dashboard/stats");
|
||||
expect(statsMapping?.validate).toEqual(["eaten", "burned", "remaining", "steps"]);
|
||||
});
|
||||
});
|
||||
@@ -117,6 +117,7 @@ import scrutiny from "./scrutiny/widget";
|
||||
import seerr from "./seerr/widget";
|
||||
import slskd from "./slskd/widget";
|
||||
import sonarr from "./sonarr/widget";
|
||||
import sparkyfitness from "./sparkyfitness/widget";
|
||||
import speedtest from "./speedtest/widget";
|
||||
import spoolman from "./spoolman/widget";
|
||||
import stash from "./stash/widget";
|
||||
@@ -274,6 +275,7 @@ const widgets = {
|
||||
seerr,
|
||||
slskd,
|
||||
sonarr,
|
||||
sparkyfitness,
|
||||
speedtest,
|
||||
spoolman,
|
||||
stash,
|
||||
|
||||
Reference in New Issue
Block a user