"use client";
import { useCallback, useMemo, useState } from "react";
import { Button, Divider, Group, Stack, Text, Title, Tooltip } from "@mantine/core";
import {
IconArchive,
IconCircleDot,
IconCircleFilled,
IconExternalLink,
IconGitFork,
IconProgressCheck,
IconStar,
IconTriangleFilled,
} from "@tabler/icons-react";
import combineClasses from "clsx";
import { useFormatter, useNow } from "next-intl";
import ReactMarkdown from "react-markdown";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import { isDateWithin, splitToChunksWithNItems } from "@homarr/common";
import { useScopedI18n } from "@homarr/translation/client";
import { MaskedOrNormalImage } from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.scss";
import { Providers } from "./releases-providers";
import type { ReleasesRepositoryResponse } from "./releases-repository";
const formatRelativeDate = (value: string): string => {
const isMonths = /\d+m/g.test(value);
const isOtherUnits = /\d+[HDWY]/g.test(value);
return isMonths ? value.toUpperCase() : isOtherUnits ? value.toLowerCase() : value;
};
export default function ReleasesWidget({ options }: WidgetComponentProps<"releases">) {
const t = useScopedI18n("widget.releases");
const now = useNow();
const formatter = useFormatter();
const board = useRequiredBoard();
const [expandedRepository, setExpandedRepository] = useState({ providerKey: "", identifier: "" });
const hasIconColor = useMemo(() => board.iconColor !== null, [board.iconColor]);
const relativeDateOptions = useMemo(
() => ({
newReleaseWithin: formatRelativeDate(options.newReleaseWithin),
staleReleaseWithin: formatRelativeDate(options.staleReleaseWithin),
}),
[options.newReleaseWithin, options.staleReleaseWithin],
);
const batchedRepositories = useMemo(() => splitToChunksWithNItems(options.repositories, 5), [options.repositories]);
const [results] = clientApi.useSuspenseQueries((t) =>
batchedRepositories.flatMap((chunk) =>
t.widget.releases.getLatest({
repositories: chunk.map((repository) => ({
providerKey: repository.providerKey,
identifier: repository.identifier,
versionFilter: repository.versionFilter,
})),
}),
),
);
const repositories = useMemo(() => {
return results
.flat()
.map(({ data }) => {
if (data === undefined) return undefined;
const repository = options.repositories.find(
(repository) => repository.providerKey === data.providerKey && repository.identifier === data.identifier,
);
if (repository === undefined) return undefined;
return {
...data,
iconUrl: repository.iconUrl,
isNewRelease:
relativeDateOptions.newReleaseWithin !== "" && data.latestReleaseAt
? isDateWithin(data.latestReleaseAt, relativeDateOptions.newReleaseWithin)
: false,
isStaleRelease:
relativeDateOptions.staleReleaseWithin !== "" && data.latestReleaseAt
? !isDateWithin(data.latestReleaseAt, relativeDateOptions.staleReleaseWithin)
: false,
};
})
.filter(
(repository) =>
repository !== undefined &&
(repository.error !== undefined ||
!options.showOnlyHighlighted ||
repository.isNewRelease ||
repository.isStaleRelease),
)
.sort((repoA, repoB) => {
if (repoA?.latestReleaseAt === undefined) return 1;
if (repoB?.latestReleaseAt === undefined) return -1;
return repoA.latestReleaseAt > repoB.latestReleaseAt ? -1 : 1;
}) as ReleasesRepositoryResponse[];
}, [
results,
options.repositories,
options.showOnlyHighlighted,
relativeDateOptions.newReleaseWithin,
relativeDateOptions.staleReleaseWithin,
]);
const toggleExpandedRepository = useCallback(
(repository: ReleasesRepositoryResponse) => {
if (
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier
) {
setExpandedRepository({ providerKey: "", identifier: "" });
} else {
setExpandedRepository({ providerKey: repository.providerKey, identifier: repository.identifier });
}
},
[expandedRepository],
);
return (
{repositories.map((repository: ReleasesRepositoryResponse) => {
const isActive =
expandedRepository.providerKey === repository.providerKey &&
expandedRepository.identifier === repository.identifier;
const hasError = repository.error !== undefined;
return (
toggleExpandedRepository(repository)}
>
{repository.identifier}
{hasError ? t("error.label") : (repository.latestRelease ?? t("not-found"))}
{repository.latestReleaseAt &&
!hasError &&
formatter.relativeTime(repository.latestReleaseAt, {
now,
style: "narrow",
})}
{!hasError ? (
(repository.isNewRelease || repository.isStaleRelease) && (
)
) : (
)}
{options.showDetails && (
)}
{isActive && }
);
})}
);
}
interface DetailsDisplayProps {
repository: ReleasesRepositoryResponse;
toggleExpandedRepository: (repository: ReleasesRepositoryResponse) => void;
}
const DetailsDisplay = ({ repository, toggleExpandedRepository }: DetailsDisplayProps) => {
const t = useScopedI18n("widget.releases");
const formatter = useFormatter();
return (
<>
toggleExpandedRepository(repository)} />
toggleExpandedRepository(repository)}
>
{!repository.starsCount
? "-"
: formatter.number(repository.starsCount, {
notation: "compact",
maximumFractionDigits: 1,
})}
{!repository.forksCount
? "-"
: formatter.number(repository.forksCount, {
notation: "compact",
maximumFractionDigits: 1,
})}
{!repository.openIssues
? "-"
: formatter.number(repository.openIssues, {
notation: "compact",
maximumFractionDigits: 1,
})}
>
);
};
interface ExtendedDisplayProps {
repository: ReleasesRepositoryResponse;
hasIconColor: boolean;
}
const ExpandedDisplay = ({ repository, hasIconColor }: ExtendedDisplayProps) => {
const t = useScopedI18n("widget.releases");
const now = useNow();
const formatter = useFormatter();
return (
<>
{Providers[repository.providerKey]?.name}
{repository.createdAt && (
{t("created")}
|
{formatter.relativeTime(repository.createdAt, {
now,
style: "narrow",
})}
)}
{(repository.releaseUrl ?? repository.projectUrl) && (
<>
>
)}
{repository.error && (
<>
{t("error.label")}
{repository.error.code ? t(`error.options.${repository.error.code}` as never) : repository.error.message}
>
)}
{repository.releaseDescription && (
<>
{t("releaseDescription")}
{repository.releaseDescription}
>
)}
>
);
};