| 
 | 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 | +}  | 
0 commit comments