Skip to content

Commit b3f1ecc

Browse files
committed
featI(cloud): add changelog
1 parent c434bc8 commit b3f1ecc

File tree

6 files changed

+242
-15
lines changed

6 files changed

+242
-15
lines changed

frontend/src/app/changelog.tsx

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { faSparkle, Icon } from "@rivet-gg/icons";
2+
import { useSuspenseQuery } from "@tanstack/react-query";
3+
import { useLocalStorage } from "usehooks-ts";
4+
import {
5+
Avatar,
6+
AvatarFallback,
7+
AvatarImage,
8+
Badge,
9+
cn,
10+
Picture,
11+
PictureFallback,
12+
PictureImage,
13+
Skeleton,
14+
Slot,
15+
WithTooltip,
16+
} from "@/components";
17+
import { changelogQueryOptions } from "@/queries/global";
18+
import type { ChangelogItem } from "@/queries/types";
19+
20+
interface ChangelogEntryProps extends ChangelogItem {
21+
isNew?: boolean;
22+
}
23+
24+
export function ChangelogEntry({
25+
published,
26+
images,
27+
title,
28+
description,
29+
slug,
30+
authors,
31+
isNew,
32+
}: ChangelogEntryProps) {
33+
return (
34+
<div className="py-2">
35+
<div className="flex my-2 justify-between items-center">
36+
<div className="flex items-center gap-2">
37+
<div className="bg-white text-background size-8 rounded-full flex items-center justify-center">
38+
<Icon icon={faSparkle} className="m-0" />
39+
</div>
40+
<h4 className="font-bold text-lg text-foreground">
41+
{isNew ? (
42+
<span>New Update</span>
43+
) : (
44+
<span>Latest Update</span>
45+
)}
46+
</h4>
47+
</div>
48+
<Badge variant="outline">
49+
{new Date(published).toLocaleDateString()}
50+
</Badge>
51+
</div>
52+
53+
<a
54+
href={`https://rivet.gg/changelog/${slug}`}
55+
target="_blank"
56+
rel="noreferrer"
57+
className="block"
58+
>
59+
<Picture className="rounded-md border my-4 h-[200px] w-full block overflow-hidden aspect-video">
60+
<PictureFallback>
61+
<Skeleton className="size-full" />
62+
</PictureFallback>
63+
<PictureImage
64+
className="size-full object-cover animate-in fade-in-0 duration-300 fill-mode-forwards"
65+
src={`https://rivet.gg/${images[0].url}`}
66+
width={images[0].width}
67+
height={images[0].height}
68+
alt={"Changelog entry"}
69+
/>
70+
</Picture>
71+
72+
<p className="font-semibold text-sm">{title}</p>
73+
74+
<p className="text-xs mt-1 text-muted-foreground">
75+
{description}{" "}
76+
<span className="text-right text-xs inline gap-1.5 text-foreground items-center">
77+
Read more...
78+
</span>
79+
</p>
80+
</a>
81+
<div className="flex items-end justify-end mt-2">
82+
<div className="flex gap-2 items-center">
83+
<a
84+
className="flex gap-1.5 items-center flex-row-reverse text-right"
85+
href={authors[0].socials.twitter}
86+
>
87+
<Avatar className="size-8">
88+
<AvatarFallback>
89+
{authors[0].name[0]}
90+
</AvatarFallback>
91+
<AvatarImage
92+
src={`https://rivet.gg/${authors[0].avatar.url}`}
93+
alt={authors[0].name}
94+
/>
95+
</Avatar>
96+
<div className="ml-2">
97+
<p className="font-semibold text-sm">
98+
{authors[0].name}
99+
</p>
100+
<p className="text-xs text-muted-foreground">
101+
{authors[0].role}
102+
</p>
103+
</div>
104+
</a>
105+
</div>
106+
</div>
107+
</div>
108+
);
109+
}
110+
interface ChangelogProps {
111+
className?: string;
112+
children?: React.ReactNode;
113+
}
114+
115+
export function Changelog({ className, children, ...props }: ChangelogProps) {
116+
const { data } = useSuspenseQuery(changelogQueryOptions());
117+
118+
const [lastChangelog, setLast] = useLocalStorage<string | null>(
119+
"rivet-lastchangelog",
120+
null,
121+
);
122+
123+
const hasNewChangelog = !lastChangelog
124+
? data.length > 0
125+
: data.some(
126+
(entry) => new Date(entry.published) > new Date(lastChangelog),
127+
);
128+
129+
return (
130+
<WithTooltip
131+
delayDuration={0}
132+
contentProps={{ collisionPadding: 8 }}
133+
onOpenChange={(isOpen) => {
134+
if (isOpen) {
135+
setLast(data[0].published);
136+
}
137+
}}
138+
trigger={
139+
<Slot
140+
{...props}
141+
className={cn(
142+
"relative",
143+
!hasNewChangelog && "[&_[data-changelog-ping]]:hidden",
144+
className,
145+
)}
146+
>
147+
{children}
148+
</Slot>
149+
}
150+
content={<ChangelogEntry {...data[0]} isNew={hasNewChangelog} />}
151+
/>
152+
);
153+
}

frontend/src/app/data-providers/engine-data-provider.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,7 @@ export const createNamespaceContext = ({
391391
},
392392
createRunnerConfigMutationOptions(
393393
opts: {
394-
onSuccess?: (
395-
data: Rivet.NamespacesRunnerConfigsUpsertResponse,
396-
) => void;
394+
onSuccess?: (data: Rivet.RunnerConfigsUpsertResponse) => void;
397395
} = {},
398396
) {
399397
return {
@@ -404,14 +402,12 @@ export const createNamespaceContext = ({
404402
config,
405403
}: {
406404
name: string;
407-
config: Rivet.NamespacesRunnerConfig;
405+
config: Rivet.RunnerConfig;
408406
}) => {
409-
const response =
410-
await client.namespacesRunnerConfigs.upsert(
411-
namespaceId,
412-
name,
413-
config,
414-
);
407+
const response = await client.runnerConfigs.upsert(name, {
408+
namespace,
409+
...config,
410+
});
415411
return response;
416412
},
417413
};
@@ -421,9 +417,9 @@ export const createNamespaceContext = ({
421417
queryKey: [{ namespace }, "runners", "configs"],
422418
initialPageParam: undefined as string | undefined,
423419
queryFn: async ({ signal: abortSignal, pageParam }) => {
424-
const response = await client.namespacesRunnerConfigs.list(
425-
namespace,
420+
const response = await client.runnerConfigs.list(
426421
{
422+
namespace,
427423
cursor: pageParam ?? undefined,
428424
limit: RECORDS_PER_PAGE,
429425
},

frontend/src/app/layout.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
cn,
3535
DocsSheet,
3636
type ImperativePanelHandle,
37+
Ping,
3738
ResizableHandle,
3839
ResizablePanel,
3940
ResizablePanelGroup,
@@ -44,6 +45,7 @@ import { useInspectorDataProvider } from "@/components/actors";
4445
import type { HeaderLinkProps } from "@/components/header/header-link";
4546
import { ensureTrailingSlash } from "@/lib/utils";
4647
import { ActorBuildsList } from "./actor-builds-list";
48+
import { Changelog } from "./changelog";
4749
import { ContextSwitcher } from "./context-switcher";
4850
import { useInspectorCredentials } from "./credentials-context";
4951
import { NamespaceSelect } from "./namespace-select";
@@ -185,6 +187,26 @@ const Sidebar = ({
185187
__APP_TYPE__ !== "cloud" ? "pb-4" : "",
186188
)}
187189
>
190+
<Changelog>
191+
<Button
192+
className="text-muted-foreground justify-start py-1 h-auto"
193+
variant="ghost"
194+
size="xs"
195+
asChild
196+
>
197+
<a
198+
href="https://rivet.gg/changelog"
199+
target="_blank"
200+
rel="noopener"
201+
>
202+
Whats new?
203+
<Ping
204+
className="relative -right-1"
205+
data-changelog-ping
206+
/>
207+
</a>
208+
</Button>
209+
</Changelog>
188210
<DocsSheet
189211
path={"https://rivet.gg/docs"}
190212
title="Documentation"

frontend/src/components/ui/tooltip.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,20 @@ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
3030
interface WithTooltipProps extends TooltipPrimitive.TooltipProps {
3131
trigger: React.ReactNode;
3232
content: React.ReactNode;
33+
contentProps?: React.ComponentPropsWithoutRef<typeof TooltipContent>;
3334
}
3435

35-
const WithTooltip = ({ trigger, content, ...rest }: WithTooltipProps) => {
36+
const WithTooltip = ({
37+
trigger,
38+
content,
39+
contentProps,
40+
...rest
41+
}: WithTooltipProps) => {
3642
return (
3743
<Tooltip {...rest}>
3844
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
3945
<TooltipPrimitive.TooltipPortal>
40-
<TooltipContent>{content}</TooltipContent>
46+
<TooltipContent {...contentProps}>{content}</TooltipContent>
4147
</TooltipPrimitive.TooltipPortal>
4248
</Tooltip>
4349
);

frontend/src/queries/global.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
1+
import {
2+
MutationCache,
3+
QueryCache,
4+
QueryClient,
5+
queryOptions,
6+
} from "@tanstack/react-query";
27
import { toast } from "@/components";
8+
import { Changelog } from "./types";
39

410
const queryCache = new QueryCache();
511

@@ -14,6 +20,23 @@ const mutationCache = new MutationCache({
1420
},
1521
});
1622

23+
export const changelogQueryOptions = () => {
24+
return queryOptions({
25+
queryKey: ["changelog", __APP_BUILD_ID__],
26+
staleTime: 1 * 60 * 60 * 1000, // 1 hour
27+
queryFn: async () => {
28+
const response = await fetch(
29+
"https://rivet-site.vercel.app/changelog.json",
30+
);
31+
if (!response.ok) {
32+
throw new Error("Failed to fetch changelog");
33+
}
34+
const result = Changelog.parse(await response.json());
35+
return result;
36+
},
37+
});
38+
};
39+
1740
export const queryClient = new QueryClient({
1841
defaultOptions: {
1942
queries: {

frontend/src/queries/types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { z } from "zod";
2+
3+
export const ChangelogItem = z.object({
4+
published: z.string(),
5+
images: z.array(
6+
z.object({ url: z.string(), width: z.number(), height: z.number() }),
7+
),
8+
title: z.string(),
9+
description: z.string(),
10+
slug: z.string(),
11+
authors: z.array(
12+
z.object({
13+
name: z.string(),
14+
role: z.string(),
15+
avatar: z.object({ url: z.string() }),
16+
socials: z.object({
17+
twitter: z.string().optional(),
18+
github: z.string().optional(),
19+
bluesky: z.string().optional(),
20+
}),
21+
}),
22+
),
23+
});
24+
export const Changelog = z.array(ChangelogItem);
25+
26+
export type Changelog = z.infer<typeof Changelog>;
27+
export type ChangelogItem = z.infer<typeof ChangelogItem>;

0 commit comments

Comments
 (0)