|  | 
|  | 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