profile header n shit
This commit is contained in:
@@ -35,15 +35,14 @@ export default function RootLayout({
|
|||||||
<ReactQueryProvider>
|
<ReactQueryProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<main>
|
<main>
|
||||||
<Image
|
{/* <Image
|
||||||
/* window.localStorage.BgImageUrl */
|
|
||||||
src={"/bg.png"}
|
src={"/bg.png"}
|
||||||
width={1920}
|
width={1920}
|
||||||
height={1080}
|
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=""
|
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 />
|
<QuickTopUI />
|
||||||
<QuickTopUILogoPart />
|
<QuickTopUILogoPart />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
41
app/page.tsx
41
app/page.tsx
@@ -12,30 +12,40 @@ import {
|
|||||||
getOmniRecommendationsHome,
|
getOmniRecommendationsHome,
|
||||||
OmniRecommendation
|
OmniRecommendation
|
||||||
} from "@/lib/omniRecommendation";
|
} from "@/lib/omniRecommendation";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { getThumbnails, ThumbnailRequest } from "@/lib/thumbnailLoader";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AlertTriangleIcon } from "lucide-react";
|
import { AlertTriangleIcon } from "lucide-react";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const SORTS_ALLOWED_IDS = [100000003, 100000001];
|
const SORTS_ALLOWED_IDS = [100000003, 100000001];
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { data: rec } = useQuery({
|
const { data: rec, isLoading } = useQuery({
|
||||||
queryKey: ["omni-recommendations"],
|
queryKey: ["omni-recommendations"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const r = await getOmniRecommendationsHome();
|
const r = await getOmniRecommendationsHome();
|
||||||
if (r) {
|
if (r) {
|
||||||
loadThumbnails(
|
// Prefetch game thumbnails into React Query cache
|
||||||
Object.entries(r.contentMetadata.Game).map((a) => ({
|
const gameRequests: ThumbnailRequest[] = Object.entries(
|
||||||
type: "GameThumbnail",
|
r.contentMetadata.Game
|
||||||
targetId: Number(a[1].rootPlaceId),
|
).map(([_, g]) => ({
|
||||||
format: "webp",
|
type: "GameThumbnail" as const,
|
||||||
size: "384x216"
|
targetId: Number(g.rootPlaceId),
|
||||||
}))
|
format: "webp",
|
||||||
).catch((a) => {});
|
size: "384x216"
|
||||||
|
}));
|
||||||
|
|
||||||
|
await queryClient.prefetchQuery({
|
||||||
|
queryKey: [
|
||||||
|
"thumbnails",
|
||||||
|
gameRequests.map((r) => r.targetId)
|
||||||
|
],
|
||||||
|
queryFn: () => getThumbnails(gameRequests)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
},
|
},
|
||||||
staleTime: 300000, // 5 minutes
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
refetchOnWindowFocus: false
|
refetchOnWindowFocus: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,18 +60,19 @@ export default function Home() {
|
|||||||
<AlertTriangleIcon />
|
<AlertTriangleIcon />
|
||||||
<AlertTitle>Warning</AlertTitle>
|
<AlertTitle>Warning</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
This is work in progess, you can follow the development
|
This is work in progress, you can follow the development
|
||||||
process on GitHub.
|
process on GitHub.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 space-y-8 no-scrollbar">
|
<div className="p-4 space-y-8 no-scrollbar">
|
||||||
{!rec ? (
|
{isLoading || !rec ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="h-[200px] flex items-center justify-center">
|
<div className="h-[200px] flex items-center justify-center">
|
||||||
<div className="animate-pulse text-muted-foreground">
|
<div className="animate-pulse text-muted-foreground">
|
||||||
{"Loading..."}
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</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-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
|
"@tanstack/query-async-storage-persister": "^5.85.3",
|
||||||
"@tanstack/react-query": "^5.85.3",
|
"@tanstack/react-query": "^5.85.3",
|
||||||
"@tanstack/react-query-devtools": "^5.85.3",
|
"@tanstack/react-query-devtools": "^5.85.3",
|
||||||
|
"@tanstack/react-query-persist-client": "^5.85.3",
|
||||||
"@types/bun": "^1.2.19",
|
"@types/bun": "^1.2.19",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.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=="],
|
"@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-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-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": ["@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-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/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=="],
|
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
|
||||||
|
|||||||
@@ -1,19 +1,42 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode, useEffect } from "react";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
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 { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function ReactQueryProvider({
|
interface Props {
|
||||||
children
|
children: ReactNode;
|
||||||
}: {
|
}
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
export function ReactQueryProvider({ children }: Props) {
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
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 (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
{children}
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} client={queryClient} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import React, { useMemo } from "react";
|
|||||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||||
import { StupidHoverThing } from "../util/MiscStuff";
|
import { StupidHoverThing } from "../util/MiscStuff";
|
||||||
import { VerifiedIcon } from "./RobloxIcons";
|
import { VerifiedIcon } from "./RobloxIcons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function FriendCarousel({
|
export function FriendCarousel({
|
||||||
friends: friendsUnsorted,
|
friends: friendsUnsorted,
|
||||||
@@ -122,7 +123,8 @@ export function FriendCarousel({
|
|||||||
</div>
|
</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
|
<LazyLoadedImage
|
||||||
imgId={`AvatarHeadShot_${a.id}`}
|
imgId={`AvatarHeadShot_${a.id}`}
|
||||||
alt={a.name}
|
alt={a.name}
|
||||||
@@ -141,6 +143,7 @@ export function FriendCarousel({
|
|||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
</StupidHoverThing>
|
</StupidHoverThing>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function FriendsHomeSect(
|
|||||||
) {
|
) {
|
||||||
const friends = useFriendsHome();
|
const friends = useFriendsHome();
|
||||||
|
|
||||||
return <FriendCarousel {...props} title="Friends" friends={friends} />;
|
return friends && <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BestFriendsHomeSect(
|
export function BestFriendsHomeSect(
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
|
|||||||
}
|
}
|
||||||
alt={game.name}
|
alt={game.name}
|
||||||
className="object-fill w-full h-full"
|
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">
|
<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 { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
// chatgpt + human
|
// chatgpt + human
|
||||||
function randomGreeting(name: string): string {
|
function randomGreeting(name: string): string {
|
||||||
@@ -50,12 +51,12 @@ export function HomeLoggedInHeader() {
|
|||||||
userPresence === 1
|
userPresence === 1
|
||||||
? "border-blue/25 bg-blue/25"
|
? "border-blue/25 bg-blue/25"
|
||||||
: userPresence === 2
|
: userPresence === 2
|
||||||
? "border-green/25 bg-green/25"
|
? "border-green/25 bg-green/25"
|
||||||
: userPresence === 3
|
: userPresence === 3
|
||||||
? "border-yellow/25 bg-yellow/25"
|
? "border-yellow/25 bg-yellow/25"
|
||||||
: userPresence === 0
|
: userPresence === 0
|
||||||
? "border-surface2/25 bg-surface2/25"
|
? "border-surface2/25 bg-surface2/25"
|
||||||
: "border-red/25 bg-red/25";
|
: "border-red/25 bg-red/25";
|
||||||
|
|
||||||
const isLoaded = !!profile && !!accountSettings;
|
const isLoaded = !!profile && !!accountSettings;
|
||||||
|
|
||||||
@@ -91,11 +92,13 @@ export function HomeLoggedInHeader() {
|
|||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center">
|
||||||
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
||||||
{isLoaded ? (
|
{isLoaded ? (
|
||||||
randomGreeting(
|
<Link href={`/users/${profile.id}`}>
|
||||||
window.localStorage.UserPreferredName ||
|
{randomGreeting(
|
||||||
profile.displayName ||
|
window.localStorage.UserPreferredName ||
|
||||||
"Robloxian!"
|
profile.displayName ||
|
||||||
)
|
"Robloxian!"
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||||
|
|||||||
@@ -1,33 +1,78 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { useThumbnailURL } from "@/hooks/use-lazy-load";
|
|
||||||
|
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 { Skeleton } from "../ui/skeleton";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
interface LazyLoadedImageProps {
|
interface LazyLoadedImageProps {
|
||||||
imgId: string;
|
imgId: string;
|
||||||
alt: 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> = ({
|
const LazyLoadedImage: React.FC<LazyLoadedImageProps> = ({
|
||||||
imgId,
|
imgId,
|
||||||
alt,
|
alt,
|
||||||
|
width = 1024,
|
||||||
|
height = 1024,
|
||||||
|
className,
|
||||||
|
lazyFetch = true,
|
||||||
|
size = "48x48",
|
||||||
...props
|
...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 (
|
return (
|
||||||
<div>
|
<div ref={ref}>
|
||||||
{imgUrl ? (
|
{imgUrl && !isLoading ? (
|
||||||
<Image
|
<Image
|
||||||
src={imgUrl as any}
|
src={imgUrl as any}
|
||||||
width={1024}
|
width={width}
|
||||||
height={1024}
|
height={height}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
|
className={className}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton {...props} />
|
<Skeleton className={className} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ export function useRobuxBalance() {
|
|||||||
const { data: robux } = useQuery<number | false | null>({
|
const { data: robux } = useQuery<number | false | null>({
|
||||||
queryKey: ["robux-balance", acct ? acct.id : "acctId"],
|
queryKey: ["robux-balance", acct ? acct.id : "acctId"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!acct) return null;
|
if (!acct) return 0;
|
||||||
try {
|
try {
|
||||||
const res = await proxyFetch(
|
const res = await proxyFetch(
|
||||||
`https://economy.roblox.com/v1/users/${acct.id}/currency`
|
`https://economy.roblox.com/v1/users/${acct.id}/currency`
|
||||||
);
|
);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.robux;
|
return data.robux || 0;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return 0;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled: !!acct,
|
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";
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { addThumbnail } from "@/hooks/use-lazy-load";
|
import { addThumbnail } from "@/hooks/use-lazy-load";
|
||||||
import { proxyFetch } from "./utils";
|
import { proxyFetch } from "./utils";
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ export async function getThumbnails(
|
|||||||
): Promise<AssetThumbnail[]> {
|
): Promise<AssetThumbnail[]> {
|
||||||
const batchSize = 100;
|
const batchSize = 100;
|
||||||
const results: AssetThumbnail[] = [];
|
const results: AssetThumbnail[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < b.length; i += batchSize) {
|
for (let i = 0; i < b.length; i += batchSize) {
|
||||||
const batch = b.slice(i, i + batchSize);
|
const batch = b.slice(i, i + batchSize);
|
||||||
const data = await proxyFetch(
|
const data = await proxyFetch(
|
||||||
@@ -49,19 +51,29 @@ export async function getThumbnails(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const json = await data.json();
|
const json = await data.json();
|
||||||
json.data.forEach((a: AssetThumbnail) => {
|
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)!;
|
||||||
const ty = b.find((c) => c.targetId == a.targetId)!;
|
addThumbnail(`${ty.type}_${a.targetId}`, a.imageUrl);
|
||||||
addThumbnail(ty.type + "_" + a.targetId.toString(), a.imageUrl);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push(...(json.data as AssetThumbnail[]));
|
results.push(...(json.data as AssetThumbnail[]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
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> {
|
export async function loadThumbnails(b: ThumbnailRequest[]): Promise<void> {
|
||||||
const th = await getThumbnails(b);
|
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-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/line-clamp": "^0.4.4",
|
"@tailwindcss/line-clamp": "^0.4.4",
|
||||||
|
"@tanstack/query-async-storage-persister": "^5.85.3",
|
||||||
"@tanstack/react-query": "^5.85.3",
|
"@tanstack/react-query": "^5.85.3",
|
||||||
"@tanstack/react-query-devtools": "^5.85.3",
|
"@tanstack/react-query-devtools": "^5.85.3",
|
||||||
|
"@tanstack/react-query-persist-client": "^5.85.3",
|
||||||
"@types/bun": "^1.2.19",
|
"@types/bun": "^1.2.19",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user