profile header n shit
This commit is contained in:
@@ -35,15 +35,14 @@ export default function RootLayout({
|
||||
<ReactQueryProvider>
|
||||
<TooltipProvider>
|
||||
<main>
|
||||
<Image
|
||||
/* window.localStorage.BgImageUrl */
|
||||
{/* <Image
|
||||
src={"/bg.png"}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 blur-lg opacity-25"
|
||||
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 opacity-25"
|
||||
alt=""
|
||||
/>
|
||||
<div className="z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||
/> */}
|
||||
<div className="backdrop-blur-lg z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||
<QuickTopUI />
|
||||
<QuickTopUILogoPart />
|
||||
{children}
|
||||
|
||||
41
app/page.tsx
41
app/page.tsx
@@ -12,30 +12,40 @@ import {
|
||||
getOmniRecommendationsHome,
|
||||
OmniRecommendation
|
||||
} from "@/lib/omniRecommendation";
|
||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getThumbnails, ThumbnailRequest } from "@/lib/thumbnailLoader";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertTriangleIcon } from "lucide-react";
|
||||
|
||||
export default function Home() {
|
||||
const SORTS_ALLOWED_IDS = [100000003, 100000001];
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: rec } = useQuery({
|
||||
const { data: rec, isLoading } = useQuery({
|
||||
queryKey: ["omni-recommendations"],
|
||||
queryFn: async () => {
|
||||
const r = await getOmniRecommendationsHome();
|
||||
if (r) {
|
||||
loadThumbnails(
|
||||
Object.entries(r.contentMetadata.Game).map((a) => ({
|
||||
type: "GameThumbnail",
|
||||
targetId: Number(a[1].rootPlaceId),
|
||||
format: "webp",
|
||||
size: "384x216"
|
||||
}))
|
||||
).catch((a) => {});
|
||||
// Prefetch game thumbnails into React Query cache
|
||||
const gameRequests: ThumbnailRequest[] = Object.entries(
|
||||
r.contentMetadata.Game
|
||||
).map(([_, g]) => ({
|
||||
type: "GameThumbnail" as const,
|
||||
targetId: Number(g.rootPlaceId),
|
||||
format: "webp",
|
||||
size: "384x216"
|
||||
}));
|
||||
|
||||
await queryClient.prefetchQuery({
|
||||
queryKey: [
|
||||
"thumbnails",
|
||||
gameRequests.map((r) => r.targetId)
|
||||
],
|
||||
queryFn: () => getThumbnails(gameRequests)
|
||||
});
|
||||
}
|
||||
return r;
|
||||
},
|
||||
staleTime: 300000, // 5 minutes
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
@@ -50,18 +60,19 @@ export default function Home() {
|
||||
<AlertTriangleIcon />
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription>
|
||||
This is work in progess, you can follow the development
|
||||
This is work in progress, you can follow the development
|
||||
process on GitHub.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-8 no-scrollbar">
|
||||
{!rec ? (
|
||||
{isLoading || !rec ? (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="h-[200px] flex items-center justify-center">
|
||||
<div className="animate-pulse text-muted-foreground">
|
||||
{"Loading..."}
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export default function Page() {
|
||||
return <>hi</>;
|
||||
}
|
||||
42
app/users/[id]/content.tsx
Normal file
42
app/users/[id]/content.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getUserByUserId } from "@/lib/profile";
|
||||
import { UserProfileHeader } from "@/components/roblox/UserProfileHeader";
|
||||
|
||||
interface UserProfileContentProps {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export default function UserProfileContent({
|
||||
userId
|
||||
}: UserProfileContentProps) {
|
||||
const { data: profile, isLoading } = useQuery({
|
||||
queryKey: ["user-profile", userId],
|
||||
queryFn: () => getUserByUserId(userId),
|
||||
enabled: !!userId
|
||||
});
|
||||
|
||||
// Set dynamic document title
|
||||
useEffect(() => {
|
||||
if (profile?.displayName) {
|
||||
document.title = `${profile.displayName}'s profile | ocbwoy3-chan's roblox`;
|
||||
}
|
||||
}, [profile]);
|
||||
|
||||
if (isLoading) return <div className="p-4">Loading user profile...</div>;
|
||||
if (!profile) notFound();
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6">
|
||||
<UserProfileHeader user={profile} />
|
||||
<Separator />
|
||||
<div className="break-all whitespace-normal">
|
||||
{profile.description}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
app/users/[id]/page.tsx
Normal file
11
app/users/[id]/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Suspense } from "react";
|
||||
import UserProfileContent from "./content";
|
||||
|
||||
// page.tsx (Server Component)
|
||||
export default async function UserProfilePage({ params }: { params: { id: string } }) {
|
||||
return (
|
||||
<Suspense fallback={<div className="p-4">Loading profile…</div>}>
|
||||
<UserProfileContent userId={(await params).id} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
8
bun.lock
8
bun.lock
@@ -35,8 +35,10 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.3",
|
||||
"@tanstack/react-query": "^5.85.3",
|
||||
"@tanstack/react-query-devtools": "^5.85.3",
|
||||
"@tanstack/react-query-persist-client": "^5.85.3",
|
||||
"@types/bun": "^1.2.19",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -287,14 +289,20 @@
|
||||
|
||||
"@tailwindcss/line-clamp": ["@tailwindcss/line-clamp@0.4.4", "", { "peerDependencies": { "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g=="],
|
||||
|
||||
"@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.85.3", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.85.3" } }, "sha512-lcyIZBMuW7iI1oJCvhQoOinouzl1kd9Fkc9rHwgO/D7Y1sfeDNU7PdMgGAEYc1MZelU84A6LotzcND56iXTxfw=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.85.3", "", {}, "sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ=="],
|
||||
|
||||
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.84.0", "", {}, "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ=="],
|
||||
|
||||
"@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.85.3", "", { "dependencies": { "@tanstack/query-core": "5.85.3" } }, "sha512-b6PfSvBbxr1ZGk1bt6aysJr3RcQBkWDzj8HtZUFHDSvSBzWipRtPYPpvJBPwmWKkVPjIKUo40lBsEA0drRgvvA=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.85.3", "", { "dependencies": { "@tanstack/query-core": "5.85.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ=="],
|
||||
|
||||
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.85.3", "", { "dependencies": { "@tanstack/query-devtools": "5.84.0" }, "peerDependencies": { "@tanstack/react-query": "^5.85.3", "react": "^18 || ^19" } }, "sha512-WSVweCE1Kh1BVvPDHAmLgGT+GGTJQ9+a7bVqzD+zUiUTht+salJjYm5nikpMNaHFPJV102TCYdvgHgBXtURRNg=="],
|
||||
|
||||
"@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.85.3", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.85.3" }, "peerDependencies": { "@tanstack/react-query": "^5.85.3", "react": "^18 || ^19" } }, "sha512-FiQ2zHGwtWMeBt3elkINcxKxiO3FM/U3Q6fZXEIWiZGuoS9DK8WV1uEwOgb66ZyzI8z2OyJzLkePSD82fGD5Rw=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { persistQueryClient } from "@tanstack/react-query-persist-client";
|
||||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ReactQueryProvider({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ReactQueryProvider({ children }: Props) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
retry: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Persist to localStorage (safe, runs client-side)
|
||||
const localStoragePersister = createAsyncStoragePersister({
|
||||
storage: window.localStorage
|
||||
});
|
||||
|
||||
persistQueryClient({
|
||||
queryClient,
|
||||
persister: localStoragePersister,
|
||||
maxAge: 1000 * 60 * 60 // 1 hour max
|
||||
});
|
||||
}, [window || "wtf"]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} client={queryClient} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useMemo } from "react";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import { StupidHoverThing } from "../util/MiscStuff";
|
||||
import { VerifiedIcon } from "./RobloxIcons";
|
||||
import Link from "next/link";
|
||||
|
||||
export function FriendCarousel({
|
||||
friends: friendsUnsorted,
|
||||
@@ -122,7 +123,8 @@ export function FriendCarousel({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col min-w-[6.5rem]">
|
||||
<Link href={`/users/${a.id}`}>
|
||||
<div className="flex flex-col min-w-[6.5rem]">
|
||||
<LazyLoadedImage
|
||||
imgId={`AvatarHeadShot_${a.id}`}
|
||||
alt={a.name}
|
||||
@@ -141,6 +143,7 @@ export function FriendCarousel({
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</StupidHoverThing>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function FriendsHomeSect(
|
||||
) {
|
||||
const friends = useFriendsHome();
|
||||
|
||||
return <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||
return friends && <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||
}
|
||||
|
||||
export function BestFriendsHomeSect(
|
||||
|
||||
@@ -29,6 +29,8 @@ export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
|
||||
}
|
||||
alt={game.name}
|
||||
className="object-fill w-full h-full"
|
||||
lazyFetch={false} // ALWAYS fetch immediately
|
||||
size="384x216" // match game thumbnail size
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-muted">
|
||||
|
||||
117
components/roblox/UserProfileHeader.tsx
Normal file
117
components/roblox/UserProfileHeader.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import { OctagonXIcon } from "lucide-react";
|
||||
import {
|
||||
RobloxPremiumSmall,
|
||||
RobloxVerifiedSmall
|
||||
} from "@/components/roblox/RobloxTooltips";
|
||||
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||
import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
|
||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { UserProfileDetails } from "@/lib/profile";
|
||||
|
||||
export function UserProfileHeader({ user }: { user: UserProfileDetails }) {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="justify-center w-screen px-8 py-6">
|
||||
<Alert variant="destructive" className="bg-base/50 space-x-2">
|
||||
<OctagonXIcon />
|
||||
<AlertTitle>Failed to fetch account info</AlertTitle>
|
||||
<AlertDescription>
|
||||
Is it a React Query bug?
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const presence = useFriendsPresence(user ? [user.id] : []);
|
||||
|
||||
const userActivity = presence.find((b) => b.userId === user?.id);
|
||||
const userPresence = userActivity?.userPresenceType;
|
||||
const borderColor =
|
||||
userPresence === 1
|
||||
? "border-blue/25 bg-blue/25"
|
||||
: userPresence === 2
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
|
||||
const isLoaded = !!user;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <button onClick={()=>console.log(userPresence)}>debug this</button> */}
|
||||
<div
|
||||
className="flex items-center gap-6 rounded-xl px-8 py-6 w-fit mt-8 ml-0"
|
||||
onContextMenu={(e) => {
|
||||
if (e.button === 2) {
|
||||
toast("[debug] reloading user pfp");
|
||||
console.log("[debug] reloading user pfp");
|
||||
loadThumbnails([
|
||||
{
|
||||
type: "AvatarHeadShot",
|
||||
targetId: user ? user.id : 1,
|
||||
format: "webp",
|
||||
size: "720x720"
|
||||
}
|
||||
]).catch(() => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isLoaded ? (
|
||||
<Skeleton className="w-28 h-28 rounded-full" />
|
||||
) : (
|
||||
<LazyLoadedImage
|
||||
imgId={`AvatarHeadShot_${user.id}`}
|
||||
alt=""
|
||||
className={`w-28 h-28 rounded-full shadow-crust border-2 ${borderColor}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
||||
{isLoaded ? (
|
||||
<Link href={`/users/${user.id}`}>
|
||||
{user.displayName}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||
</>
|
||||
)}
|
||||
{isLoaded && user.hasVerifiedBadge ? (
|
||||
<RobloxVerifiedSmall className="w-6 h-6 fill-blue text-base" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-base font-mono text-subtext0 mt-1">
|
||||
{isLoaded ? (
|
||||
<>
|
||||
@{user.name}
|
||||
{!!userActivity && userPresence === 2 ? (
|
||||
<> - {userActivity.lastLocation}</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton className="w-64 h-6 rounded-lg" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||
import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
|
||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
|
||||
// chatgpt + human
|
||||
function randomGreeting(name: string): string {
|
||||
@@ -50,12 +51,12 @@ export function HomeLoggedInHeader() {
|
||||
userPresence === 1
|
||||
? "border-blue/25 bg-blue/25"
|
||||
: userPresence === 2
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
|
||||
const isLoaded = !!profile && !!accountSettings;
|
||||
|
||||
@@ -91,11 +92,13 @@ export function HomeLoggedInHeader() {
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
||||
{isLoaded ? (
|
||||
randomGreeting(
|
||||
window.localStorage.UserPreferredName ||
|
||||
profile.displayName ||
|
||||
"Robloxian!"
|
||||
)
|
||||
<Link href={`/users/${profile.id}`}>
|
||||
{randomGreeting(
|
||||
window.localStorage.UserPreferredName ||
|
||||
profile.displayName ||
|
||||
"Robloxian!"
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||
|
||||
@@ -1,33 +1,78 @@
|
||||
import React from "react";
|
||||
import { useThumbnailURL } from "@/hooks/use-lazy-load";
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getThumbnails, ThumbnailRequest } from "@/lib/thumbnailLoader";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import Image from "next/image";
|
||||
|
||||
interface LazyLoadedImageProps {
|
||||
imgId: string;
|
||||
alt: string;
|
||||
[prop: string]: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
lazyFetch?: boolean; // true for avatars, false for game thumbnails
|
||||
size?: string; // optional fetch size
|
||||
}
|
||||
|
||||
const LazyLoadedImage: React.FC<LazyLoadedImageProps> = ({
|
||||
imgId,
|
||||
alt,
|
||||
width = 1024,
|
||||
height = 1024,
|
||||
className,
|
||||
lazyFetch = true,
|
||||
size = "48x48",
|
||||
...props
|
||||
}) => {
|
||||
const imgUrl = useThumbnailURL(imgId);
|
||||
const [isVisible, setIsVisible] = useState(!lazyFetch);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [type, targetIdStr] = imgId.split("_");
|
||||
const targetId = Number(targetIdStr);
|
||||
|
||||
// React Query to fetch thumbnail when visible or immediately if lazyFetch=false
|
||||
const { data: thumbnails, isLoading } = useQuery({
|
||||
queryKey: ["thumbnails", targetId, size],
|
||||
queryFn: async () => {
|
||||
const result = await getThumbnails([
|
||||
{ type: type as any, targetId, format: "webp", size }
|
||||
]);
|
||||
return result;
|
||||
},
|
||||
enabled: isVisible,
|
||||
staleTime: 60_000 * 60 * 60 // 1 hour
|
||||
});
|
||||
|
||||
const imgUrl = thumbnails?.[0]?.imageUrl;
|
||||
|
||||
// IntersectionObserver only used if lazyFetch=true
|
||||
useEffect(() => {
|
||||
if (!lazyFetch || !ref.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => setIsVisible(entry.isIntersecting),
|
||||
{ rootMargin: "200px" }
|
||||
);
|
||||
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [lazyFetch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{imgUrl ? (
|
||||
<div ref={ref}>
|
||||
{imgUrl && !isLoading ? (
|
||||
<Image
|
||||
src={imgUrl as any}
|
||||
width={1024}
|
||||
height={1024}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton {...props} />
|
||||
<Skeleton className={className} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,15 +12,15 @@ export function useRobuxBalance() {
|
||||
const { data: robux } = useQuery<number | false | null>({
|
||||
queryKey: ["robux-balance", acct ? acct.id : "acctId"],
|
||||
queryFn: async () => {
|
||||
if (!acct) return null;
|
||||
if (!acct) return 0;
|
||||
try {
|
||||
const res = await proxyFetch(
|
||||
`https://economy.roblox.com/v1/users/${acct.id}/currency`
|
||||
);
|
||||
const data = await res.json();
|
||||
return data.robux;
|
||||
return data.robux || 0;
|
||||
} catch {
|
||||
return false;
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
enabled: !!acct,
|
||||
|
||||
17
hooks/roblox/useThumbnails.ts
Normal file
17
hooks/roblox/useThumbnails.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
getThumbnails,
|
||||
ThumbnailRequest,
|
||||
AssetThumbnail
|
||||
} from "@/lib/thumbnailLoader";
|
||||
|
||||
export function useThumbnails(requests: ThumbnailRequest[]) {
|
||||
return useQuery<AssetThumbnail[], Error>({
|
||||
queryKey: ["thumbnails", requests.map((r) => r.targetId)],
|
||||
queryFn: () => getThumbnails(requests),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
enabled: false
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { addThumbnail } from "@/hooks/use-lazy-load";
|
||||
import { proxyFetch } from "./utils";
|
||||
|
||||
@@ -28,6 +29,7 @@ export async function getThumbnails(
|
||||
): Promise<AssetThumbnail[]> {
|
||||
const batchSize = 100;
|
||||
const results: AssetThumbnail[] = [];
|
||||
|
||||
for (let i = 0; i < b.length; i += batchSize) {
|
||||
const batch = b.slice(i, i + batchSize);
|
||||
const data = await proxyFetch(
|
||||
@@ -49,19 +51,29 @@ export async function getThumbnails(
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const json = await data.json();
|
||||
json.data.forEach((a: AssetThumbnail) => {
|
||||
// match GameThumbnail from 4972273297::GameThumbnail:384x216:webp:regular and any like- string
|
||||
const ty = b.find((c) => c.targetId == a.targetId)!;
|
||||
addThumbnail(ty.type + "_" + a.targetId.toString(), a.imageUrl);
|
||||
const ty = b.find((c) => c.targetId === a.targetId)!;
|
||||
addThumbnail(`${ty.type}_${a.targetId}`, a.imageUrl);
|
||||
});
|
||||
|
||||
results.push(...(json.data as AssetThumbnail[]));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// React Query hook for caching
|
||||
export function useThumbnails(requests: ThumbnailRequest[]) {
|
||||
return useQuery({
|
||||
queryKey: ["thumbnails", requests.map((r) => r.targetId)],
|
||||
queryFn: () => getThumbnails(requests),
|
||||
staleTime: 1000 * 60 * 5 // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
// Optional helper to load without a hook
|
||||
export async function loadThumbnails(b: ThumbnailRequest[]): Promise<void> {
|
||||
const th = await getThumbnails(b);
|
||||
}
|
||||
|
||||
// https://apis.roblox.com/discovery-api/omni-recommendation
|
||||
|
||||
@@ -40,8 +40,10 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@tanstack/query-async-storage-persister": "^5.85.3",
|
||||
"@tanstack/react-query": "^5.85.3",
|
||||
"@tanstack/react-query-devtools": "^5.85.3",
|
||||
"@tanstack/react-query-persist-client": "^5.85.3",
|
||||
"@types/bun": "^1.2.19",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
Reference in New Issue
Block a user