♻️ refactor

This commit is contained in:
Fred KISSIE
2025-11-05 23:29:48 +01:00
parent f371c7df81
commit c64b102aaa
3 changed files with 189 additions and 76 deletions

View File

@@ -139,7 +139,7 @@ export function LayoutSidebar({
</div> </div>
<div className="p-4 flex flex-col gap-4 shrink-0"> <div className="p-4 flex flex-col gap-4 shrink-0">
<ProductUpdates /> <ProductUpdates isCollapsed={isSidebarCollapsed} />
{build === "enterprise" && ( {build === "enterprise" && (
<div className="mb-3"> <div className="mb-3">

View File

@@ -3,45 +3,159 @@
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { useLocalStorage } from "@app/hooks/useLocalStorage"; import { useLocalStorage } from "@app/hooks/useLocalStorage";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { productUpdatesQueries } from "@app/lib/queries"; import { type ProductUpdate, productUpdatesQueries } from "@app/lib/queries";
import { useQueries } from "@tanstack/react-query"; import { useQueries } from "@tanstack/react-query";
import { ArrowRight, BellIcon, XIcon } from "lucide-react"; import { ArrowRight, BellIcon, ChevronRightIcon, XIcon } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Transition } from "@headlessui/react";
import * as React from "react";
export default function ProductUpdates() { export default function ProductUpdates({
isCollapsed
}: {
isCollapsed?: boolean;
}) {
const data = useQueries({ const data = useQueries({
queries: [ queries: [
productUpdatesQueries.list, productUpdatesQueries.list,
productUpdatesQueries.latestVersion productUpdatesQueries.latestVersion
], ],
combine(result) { combine(result) {
if (result[0].isLoading || result[1].isLoading) return null;
return { return {
updates: result[0].data?.data ?? [], updates: result[0].data?.data ?? [],
latestVersion: result[1].data latestVersion: result[1].data
}; };
} }
}); });
const { env } = useEnvContext();
const t = useTranslations(); const t = useTranslations();
const [showMoreUpdatesText, setShowMoreUpdatesText] = React.useState(false);
// we need to delay the initial
React.useEffect(() => {
const timeout = setTimeout(() => setShowMoreUpdatesText(true), 500);
return () => clearTimeout(timeout);
}, []);
const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage<
string | null
>("ignored-version", null);
const [showNewVersionPopup, setShowNewVersionPopup] = React.useState(true);
if (!data) return null;
// const showNewVersionPopup = Boolean(
// data?.latestVersion?.data &&
// ignoredVersionUpdate !==
// data.latestVersion.data?.pangolin.latestVersion &&
// env.app.version !== data.latestVersion.data?.pangolin.latestVersion
// );
return ( return (
<div className="flex flex-col gap-1 overflow-clip"> <div
className={cn(
"flex flex-col gap-1 overflow-clip",
isCollapsed && "hidden"
)}
>
<NewVersionAvailable
version={data.latestVersion?.data}
onClose={() => {
// setIgnoredVersionUpdate(
// data.latestVersion?.data?.pangolin.latestVersion ?? null
// );
setShowNewVersionPopup(false);
}}
show={showNewVersionPopup}
/>
<Transition show={showMoreUpdatesText}>
<small
className={cn(
"text-xs text-muted-foreground flex items-center gap-1 mt-2",
"transition ease-in duration-300 data-closed:opacity-0"
)}
>
{data.updates.length > 0 && ( {data.updates.length > 0 && (
<small className="text-xs text-muted-foreground flex items-center gap-1"> <>
<BellIcon className="flex-none size-3" /> <BellIcon className="flex-none size-3" />
<span> <span>
{t("productUpdateMoreInfo", { {showNewVersionPopup
? t("productUpdateMoreInfo", {
noOfUpdates: data.updates.length
})
: t("productUpdateInfo", {
noOfUpdates: data.updates.length noOfUpdates: data.updates.length
})} })}
</span> </span>
</small> </>
)} )}
<NewVersionAvailable version={data.latestVersion} /> </small>
</Transition>
<ProductUpdatesPopup
updates={data.updates}
show={data.updates.length > 0}
/>
</div> </div>
); );
} }
type ProductUpdatesPopupProps = { updates: ProductUpdate[]; show: boolean };
function ProductUpdatesPopup({ updates, show }: ProductUpdatesPopupProps) {
const [open, setOpen] = React.useState(false);
const t = useTranslations();
// we need to delay the initial opening state to have an animation on `appear`
React.useEffect(() => {
if (show) {
requestAnimationFrame(() => setOpen(true));
}
}, [show]);
return (
<Transition show={open}>
<div
className={cn(
"rounded-md border bg-muted p-2 py-3 w-full flex items-start gap-2 text-sm",
"transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full"
)}
>
<div className="rounded-md bg-muted-foreground/20 p-2">
<BellIcon className="flex-none size-4" />
</div>
<div className="flex flex-col gap-2">
<p className="font-medium">What's new</p>
<small
className={cn(
"text-muted-foreground",
"overflow-hidden h-8",
"[-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]"
)}
>
{updates[0].contents}
</small>
</div>
<button
className="p-1 cursor-pointer"
onClick={() => {
setOpen(false);
// onClose();
}}
>
<ChevronRightIcon className="size-4 flex-none" />
</button>
</div>
</Transition>
);
}
type NewVersionAvailableProps = { type NewVersionAvailableProps = {
onClose: () => void;
show: boolean;
version: version:
| Awaited< | Awaited<
ReturnType< ReturnType<
@@ -49,36 +163,35 @@ type NewVersionAvailableProps = {
typeof productUpdatesQueries.latestVersion.queryFn typeof productUpdatesQueries.latestVersion.queryFn
> >
> >
> >["data"]
| undefined; | undefined;
}; };
function NewVersionAvailable({ version }: NewVersionAvailableProps) { function NewVersionAvailable({
const { env } = useEnvContext(); version,
console.log({ show,
env onClose
}); }: NewVersionAvailableProps) {
const t = useTranslations(); const t = useTranslations();
const [open, setOpen] = React.useState(false);
const [ignoredVersionUpdate, setIgnoredVersionUpdate] = useLocalStorage< // we need to delay the initial opening state to have an animation on `appear`
string | null React.useEffect(() => {
>("ignored-version", null); if (show) {
requestAnimationFrame(() => setOpen(true));
const showNewVersionPopup = }
version?.data && }, [show]);
ignoredVersionUpdate !== version.data?.pangolin.latestVersion &&
env.app.version !== version.data?.pangolin.latestVersion;
if (!showNewVersionPopup) return null;
return ( return (
<Transition show={open}>
<div <div
className={cn( className={cn(
"rounded-md border bg-muted p-2 py-3 w-full flex items-start gap-2 text-sm", "rounded-md border bg-muted p-2 py-3 w-full flex items-start gap-2 text-sm",
"animate-in slide-in-from-bottom duration-300" "transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full"
)} )}
> >
{version?.data && ( {version && (
<> <>
<div className="rounded-md bg-muted-foreground/20 p-2"> <div className="rounded-md bg-muted-foreground/20 p-2">
<BellIcon className="flex-none size-4" /> <BellIcon className="flex-none size-4" />
@@ -89,11 +202,11 @@ function NewVersionAvailable({ version }: NewVersionAvailableProps) {
</p> </p>
<small className="text-muted-foreground"> <small className="text-muted-foreground">
{t("pangolinUpdateAvailableInfo", { {t("pangolinUpdateAvailableInfo", {
version: version.data.pangolin.latestVersion version: version.pangolin.latestVersion
})} })}
</small> </small>
<a <a
href={version.data.pangolin.releaseNotes} href={version.pangolin.releaseNotes}
target="_blank" target="_blank"
className="inline-flex items-center gap-0.5 text-xs font-medium" className="inline-flex items-center gap-0.5 text-xs font-medium"
> >
@@ -105,16 +218,16 @@ function NewVersionAvailable({ version }: NewVersionAvailableProps) {
</div> </div>
<button <button
className="p-1 cursor-pointer" className="p-1 cursor-pointer"
onClick={() => onClick={() => {
setIgnoredVersionUpdate( setOpen(false);
version.data?.pangolin.latestVersion ?? null onClose();
) }}
}
> >
<XIcon className="size-4 flex-none" /> <XIcon className="size-4 flex-none" />
</button> </button>
</> </>
)} )}
</div> </div>
</Transition>
); );
} }

View File

@@ -4,7 +4,7 @@ import { build } from "@server/build";
import { remote } from "./api"; import { remote } from "./api";
import type ResponseT from "@server/types/Response"; import type ResponseT from "@server/types/Response";
type ProductUpdate = { export type ProductUpdate = {
link: string | null; link: string | null;
edition: "enterprise" | "community" | "cloud" | null; edition: "enterprise" | "community" | "cloud" | null;
id: number; id: number;