react query ftw
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
// chatgpt
|
// chatgpt
|
||||||
function rewriteCookieDomain(rawCookie: string): string {
|
function rewriteCookieDomain(rawCookie: string): string {
|
||||||
return rawCookie
|
return rawCookie
|
||||||
.replace(/;?\s*Domain=[^;]+/i, '')
|
.replace(/;?\s*Domain=[^;]+/i, "")
|
||||||
.concat(`; Domain=localhost:3000`);
|
.concat(`; Domain=localhost:3000`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TooltipProvider } from "@/components/ui/tooltip";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { QuickTopUI, QuickTopUILogoPart } from "@/components/site/QuickTopUI";
|
import { QuickTopUI, QuickTopUILogoPart } from "@/components/site/QuickTopUI";
|
||||||
|
import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -31,24 +32,26 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden`}
|
||||||
>
|
>
|
||||||
<TooltipProvider>
|
<ReactQueryProvider>
|
||||||
<main>
|
<TooltipProvider>
|
||||||
<Image
|
<main>
|
||||||
/* window.localStorage.BgImageUrl */
|
<Image
|
||||||
src={"/bg.png"}
|
/* window.localStorage.BgImageUrl */
|
||||||
width={1920}
|
src={"/bg.png"}
|
||||||
height={1080}
|
width={1920}
|
||||||
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 blur-lg opacity-25"
|
height={1080}
|
||||||
alt=""
|
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 blur-lg opacity-25"
|
||||||
/>
|
alt=""
|
||||||
<div className="z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
/>
|
||||||
<QuickTopUI />
|
<div className="z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||||
<QuickTopUILogoPart />
|
<QuickTopUI />
|
||||||
{children}
|
<QuickTopUILogoPart />
|
||||||
</div>
|
{children}
|
||||||
</main>
|
</div>
|
||||||
<Toaster />
|
</main>
|
||||||
</TooltipProvider>
|
<Toaster />
|
||||||
|
</TooltipProvider>
|
||||||
|
</ReactQueryProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
17
app/page.tsx
17
app/page.tsx
@@ -13,17 +13,17 @@ import {
|
|||||||
OmniRecommendation
|
OmniRecommendation
|
||||||
} from "@/lib/omniRecommendation";
|
} from "@/lib/omniRecommendation";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { AlertTriangleIcon } from "lucide-react";
|
import { AlertTriangleIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const SORTS_ALLOWED_IDS = [100000003, 100000001];
|
const SORTS_ALLOWED_IDS = [100000003, 100000001];
|
||||||
const [rec, setRec] = useState<OmniRecommendation | null>(null);
|
|
||||||
useEffect(() => {
|
const { data: rec } = useQuery({
|
||||||
setTimeout(async () => {
|
queryKey: ["omni-recommendations"],
|
||||||
|
queryFn: async () => {
|
||||||
const r = await getOmniRecommendationsHome();
|
const r = await getOmniRecommendationsHome();
|
||||||
if (r) {
|
if (r) {
|
||||||
setRec(r);
|
|
||||||
loadThumbnails(
|
loadThumbnails(
|
||||||
Object.entries(r.contentMetadata.Game).map((a) => ({
|
Object.entries(r.contentMetadata.Game).map((a) => ({
|
||||||
type: "GameThumbnail",
|
type: "GameThumbnail",
|
||||||
@@ -33,8 +33,11 @@ export default function Home() {
|
|||||||
}))
|
}))
|
||||||
).catch((a) => {});
|
).catch((a) => {});
|
||||||
}
|
}
|
||||||
}, 1000);
|
return r;
|
||||||
}, []);
|
},
|
||||||
|
staleTime: 300000, // 5 minutes
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return <>
|
return <>hi</>;
|
||||||
hi
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|||||||
10
bun.lock
10
bun.lock
@@ -35,6 +35,8 @@
|
|||||||
"@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/react-query": "^5.85.3",
|
||||||
|
"@tanstack/react-query-devtools": "^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",
|
||||||
@@ -285,6 +287,14 @@
|
|||||||
|
|
||||||
"@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-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/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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
|
|||||||
22
components/providers/ReactQueryProvider.tsx
Normal file
22
components/providers/ReactQueryProvider.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function ReactQueryProvider({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const [queryClient] = useState(() => new QueryClient());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{children}
|
||||||
|
{process.env.NODE_ENV === "development" && (
|
||||||
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
)}
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
||||||
import { useFriendsHome } from "@/hooks/roblox/useFriends";
|
|
||||||
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||||
import React, { useEffect, useState } from "react";
|
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";
|
||||||
@@ -29,78 +28,32 @@ export function FriendCarousel({
|
|||||||
}) {
|
}) {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const presence = useFriendsPresence(
|
const presence = useFriendsPresence(
|
||||||
(!!friendsUnsorted ? friendsUnsorted : []).map((f) => f.id)
|
(friendsUnsorted || []).map((f) => f.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [friendsLabel, setFriendsLabel] = useState<string>("");
|
const friends = useMemo(() => {
|
||||||
|
if (!friendsUnsorted) return [];
|
||||||
|
|
||||||
const [friends, setFriends] = useState<
|
return [...friendsUnsorted].sort((a, b) => {
|
||||||
{
|
if (dontSortByActivity) return -10;
|
||||||
hasVerifiedBadge: boolean;
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
}[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const userStatusA = presence.find((c) => c.userId === a.id);
|
||||||
let numStudio = 0;
|
const userStatusB = presence.find((c) => c.userId === b.id);
|
||||||
let numGame = 0;
|
|
||||||
let numOnline = 0;
|
|
||||||
for (const friend of friendsUnsorted || []) {
|
|
||||||
const st = presence.find((c) => c.userId === friend.id);
|
|
||||||
switch (st?.userPresenceType || 0) {
|
|
||||||
case 1:
|
|
||||||
numOnline += 1;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
numGame += 1;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
numStudio += 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setFriendsLabel(
|
|
||||||
[
|
|
||||||
// `${friends.length}`,
|
|
||||||
(numOnline+numGame+numStudio === 0 || numOnline === 0) ? null : `${numOnline+numGame+numStudio} online`,
|
|
||||||
numGame === 0 ? null : `${numGame} in-game`,
|
|
||||||
|
|
||||||
]
|
return (
|
||||||
.filter((a) => !!a)
|
(userStatusB?.userPresenceType || 0) -
|
||||||
.join(" | ")
|
(userStatusA?.userPresenceType || 0)
|
||||||
);
|
);
|
||||||
|
});
|
||||||
if (!friendsUnsorted) {
|
|
||||||
setFriends([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFriends(
|
|
||||||
friendsUnsorted.sort((a, b) => {
|
|
||||||
if (!!dontSortByActivity) return -10;
|
|
||||||
const userStatusA = presence.find((c) => c.userId === a.id);
|
|
||||||
const userStatusB = presence.find((c) => c.userId === b.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
(userStatusB?.userPresenceType || 0) -
|
|
||||||
(userStatusA?.userPresenceType || 0)
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [friendsUnsorted, presence, dontSortByActivity]);
|
}, [friendsUnsorted, presence, dontSortByActivity]);
|
||||||
|
|
||||||
if (!friends || friends.length === 0) {
|
if (friends.length === 0) {
|
||||||
return <></>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...props}>
|
<div {...props}>
|
||||||
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
|
<h1 className="text-2xl pt-4 pl-4 -mb-4">{title}</h1>
|
||||||
<h1 className="text-2xl pt-4 pl-4 -mb-4">
|
|
||||||
{title}{" "}
|
|
||||||
<span className="text-overlay1 text-sm pl-2">{friendsLabel}</span>
|
|
||||||
</h1>
|
|
||||||
<div className="rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
|
<div className="rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
|
||||||
<div
|
<div
|
||||||
className="flex p-8 items-center gap-4 overflow-x-auto overflow-y-visible no-scrollbar pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0"
|
className="flex p-8 items-center gap-4 overflow-x-auto overflow-y-visible no-scrollbar pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0"
|
||||||
@@ -110,7 +63,6 @@ export function FriendCarousel({
|
|||||||
scrollbarWidth: "none"
|
scrollbarWidth: "none"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* <div className="w-8" /> */}
|
|
||||||
{friends.map((a) => {
|
{friends.map((a) => {
|
||||||
const userStatus = presence.find(
|
const userStatus = presence.find(
|
||||||
(b) => b.userId === a.id
|
(b) => b.userId === a.id
|
||||||
@@ -161,15 +113,16 @@ export function FriendCarousel({
|
|||||||
className={`w-4 h-4 shrink-0`}
|
className={`w-4 h-4 shrink-0`}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{ userPresence >= 2 ? <p>{userStatus?.lastLocation}</p> : <></>}
|
{userPresence >= 2 ? (
|
||||||
|
<p>
|
||||||
|
{userStatus?.lastLocation}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div className="flex flex-col min-w-[6.5rem]">
|
||||||
key={a.id}
|
|
||||||
className="flex flex-col min-w-[6.5rem]"
|
|
||||||
>
|
|
||||||
<LazyLoadedImage
|
<LazyLoadedImage
|
||||||
imgId={`AvatarHeadShot_${a.id}`}
|
imgId={`AvatarHeadShot_${a.id}`}
|
||||||
alt={a.name}
|
alt={a.name}
|
||||||
@@ -191,7 +144,6 @@ export function FriendCarousel({
|
|||||||
</StupidHoverThing>
|
</StupidHoverThing>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* <div className="w-8" /> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,5 +22,12 @@ export function BestFriendsHomeSect(
|
|||||||
) {
|
) {
|
||||||
const friends = useBestFriends();
|
const friends = useBestFriends();
|
||||||
|
|
||||||
return <FriendCarousel {...props} title="Best Friends" dontSortByActivity friends={friends} />;
|
return (
|
||||||
|
<FriendCarousel
|
||||||
|
{...props}
|
||||||
|
title="Best Friends"
|
||||||
|
dontSortByActivity
|
||||||
|
friends={friends}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { PremiumIconSmall, VerifiedIcon } from "./RobloxIcons";
|
import { PremiumIconSmall, VerifiedIcon } from "./RobloxIcons";
|
||||||
import {
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger
|
|
||||||
} from "../ui/tooltip";
|
|
||||||
|
|
||||||
export function RobloxPremiumSmall(props: React.SVGProps<SVGSVGElement>) {
|
export function RobloxPremiumSmall(props: React.SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ import { toast } from "sonner";
|
|||||||
|
|
||||||
// chatgpt + human
|
// chatgpt + human
|
||||||
function randomGreeting(name: string): string {
|
function randomGreeting(name: string): string {
|
||||||
const greetings = [
|
const greetings = [`Howdy, ${name}`];
|
||||||
`Howdy, ${name}`
|
|
||||||
];
|
|
||||||
|
|
||||||
const index = Math.floor(Math.random() * greetings.length);
|
const index = Math.floor(Math.random() * greetings.length);
|
||||||
return greetings[index];
|
return greetings[index];
|
||||||
@@ -52,12 +50,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;
|
||||||
|
|
||||||
@@ -92,7 +90,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 ? randomGreeting(window.localStorage.UserPreferredName || profile.displayName || "Robloxian!") : (
|
{isLoaded ? (
|
||||||
|
randomGreeting(
|
||||||
|
window.localStorage.UserPreferredName ||
|
||||||
|
profile.displayName ||
|
||||||
|
"Robloxian!"
|
||||||
|
)
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
|||||||
|
|
||||||
type OutfitSelectorProps = {
|
type OutfitSelectorProps = {
|
||||||
setVisible: (visible: boolean) => void;
|
setVisible: (visible: boolean) => void;
|
||||||
updateOutfit: (outfit: { id: number }, acc: {id: number}) => Promise<void>;
|
updateOutfit: (
|
||||||
|
outfit: { id: number },
|
||||||
|
acc: { id: number }
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function OutfitSelector({ setVisible, updateOutfit }: OutfitSelectorProps) {
|
export function OutfitSelector({
|
||||||
|
setVisible,
|
||||||
|
updateOutfit
|
||||||
|
}: OutfitSelectorProps) {
|
||||||
const outfits = useAvatarOutfits();
|
const outfits = useAvatarOutfits();
|
||||||
const acc = useCurrentAccount();
|
const acc = useCurrentAccount();
|
||||||
|
|
||||||
@@ -33,7 +39,7 @@ export function OutfitSelector({ setVisible, updateOutfit }: OutfitSelectorProps
|
|||||||
key={outfit.id}
|
key={outfit.id}
|
||||||
className="hover:bg-base/50 rounded-lg"
|
className="hover:bg-base/50 rounded-lg"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
updateOutfit(outfit,acc);
|
updateOutfit(outfit, acc);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ async function updateOutfit(outfit: { id: number }, acc: { id: number }) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
loadThumbnails([
|
loadThumbnails([
|
||||||
{
|
{
|
||||||
type: "AvatarHeadShot",
|
type: "AvatarHeadShot",
|
||||||
@@ -98,7 +97,7 @@ export const QuickTopUI = React.memo(function () {
|
|||||||
const bf = useBestFriends();
|
const bf = useBestFriends();
|
||||||
useCurrentAccount();
|
useCurrentAccount();
|
||||||
|
|
||||||
useFriendsPresence([...(f ? f : []), ...(bf ? bf : [])].map(a=>a.id))
|
useFriendsPresence([...(f ? f : []), ...(bf ? bf : [])].map((a) => a.id));
|
||||||
|
|
||||||
const robux = useRobuxBalance();
|
const robux = useRobuxBalance();
|
||||||
const [isOutfitSelectorVisible, setIsOutfitSelectorVisible] =
|
const [isOutfitSelectorVisible, setIsOutfitSelectorVisible] =
|
||||||
@@ -154,9 +153,12 @@ export const QuickTopUILogoPart = React.memo(function () {
|
|||||||
<Link href="/" className="-m-1 w-8 h-8">
|
<Link href="/" className="-m-1 w-8 h-8">
|
||||||
<img src="/icon-512.webp" className="w-8 h-8" alt="" />
|
<img src="/icon-512.webp" className="w-8 h-8" alt="" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/test" className="mt-2 gap-2 flex items-center">
|
<Link href="/" className="mt-2 gap-2 flex items-center">
|
||||||
<p>{"ocbwoy3-chan's roblox"}</p>
|
<p>{"ocbwoy3-chan's roblox"}</p>
|
||||||
<p className="text-surface2 line-clamp-1">{process.env.NODE_ENV} {process.env.NEXT_PUBLIC_CWD} {process.env.NEXT_PUBLIC_ARGV0}</p>
|
{/* <p className="text-surface2 line-clamp-1">
|
||||||
|
{process.env.NODE_ENV} {process.env.NEXT_PUBLIC_CWD}{" "}
|
||||||
|
{process.env.NEXT_PUBLIC_ARGV0}
|
||||||
|
</p> */}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,15 +2,18 @@ import { TooltipProps } from "@radix-ui/react-tooltip";
|
|||||||
import { VerifiedIcon } from "../roblox/RobloxIcons";
|
import { VerifiedIcon } from "../roblox/RobloxIcons";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||||
|
|
||||||
export function StupidHoverThing({ children, text, ...props }: React.PropsWithChildren & TooltipProps & { text: string | React.ReactNode }) {
|
export function StupidHoverThing({
|
||||||
|
children,
|
||||||
|
text,
|
||||||
|
...props
|
||||||
|
}: React.PropsWithChildren &
|
||||||
|
TooltipProps & { text: string | React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Tooltip {...props}>
|
<Tooltip {...props}>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
{children}
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="bg-surface0 text-text m-2">
|
<TooltipContent className="bg-surface0 text-text m-2">
|
||||||
<span className="text-sm flex items-center">{text}</span>
|
<span className="text-sm flex items-center">{text}</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,84 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
type AccountSettings = {
|
type AccountSettings = {
|
||||||
ChangeUsernameEnabled: boolean
|
ChangeUsernameEnabled: boolean;
|
||||||
|
|
||||||
/* determines if the account owner is a roblox admin */
|
/* determines if the account owner is a roblox admin */
|
||||||
IsAdmin: boolean,
|
IsAdmin: boolean;
|
||||||
|
|
||||||
PreviousUserNames: string,
|
PreviousUserNames: string;
|
||||||
|
|
||||||
/* censored out email */
|
/* censored out email */
|
||||||
UserEmail: string,
|
UserEmail: string;
|
||||||
|
|
||||||
UserAbove13: boolean,
|
UserAbove13: boolean;
|
||||||
|
|
||||||
/* does the user have roblox premium */
|
/* does the user have roblox premium */
|
||||||
IsPremium: boolean,
|
IsPremium: boolean;
|
||||||
|
|
||||||
/* ingame chat */
|
/* ingame chat */
|
||||||
IsGameChatSettingEnabled: boolean
|
IsGameChatSettingEnabled: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useAccountSettings() {
|
export function useAccountSettings() {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [accountSettings, setAccountSettings] = useState<AccountSettings | false | null>(null);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: accountSettings } = useQuery<AccountSettings | false | null>({
|
||||||
if (!acct) return;
|
queryKey: ["account-settings", acct ? acct.id : "acctId"],
|
||||||
|
queryFn: async () => {
|
||||||
let cancelled = false;
|
if (!acct) return null;
|
||||||
|
|
||||||
const fetchSetttings = async () => {
|
|
||||||
if (!acct || cancelled) return;
|
|
||||||
try {
|
try {
|
||||||
const res = await proxyFetch(
|
const res = await proxyFetch(
|
||||||
`https://www.roblox.com/my/settings/json`
|
`https://www.roblox.com/my/settings/json`
|
||||||
);
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(
|
||||||
|
`[useAccountSettings] API Error ${res.status} ${res.statusText}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!cancelled) setAccountSettings(data);
|
return data;
|
||||||
} catch {
|
} catch (error) {
|
||||||
if (!cancelled) setAccountSettings(false);
|
console.error(
|
||||||
|
"[useAccountSettings] Failed to fetch settings",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
enabled: !!acct,
|
||||||
fetchSetttings();
|
staleTime: Infinity,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
refetchOnReconnect: false
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const handleTransaction = () => {
|
const handleTransaction = () => {
|
||||||
fetchSetttings();
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["account-settings", acct ? acct.id : "acctId"]
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("settingTransactionCompletedEvent", handleTransaction);
|
window.addEventListener(
|
||||||
|
"settingTransactionCompletedEvent",
|
||||||
|
handleTransaction
|
||||||
|
);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"settingTransactionCompletedEvent",
|
"settingTransactionCompletedEvent",
|
||||||
handleTransaction
|
handleTransaction
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [acct]);
|
}, [acct ? acct.id : "acctId", queryClient]);
|
||||||
|
|
||||||
return accountSettings;
|
return accountSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +1,64 @@
|
|||||||
// https://avatar.roblox.com/v2/avatar/users/1083030325/outfits?isEditable=true&itemsPerPage=50&outfitType=Avatar
|
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
|
|
||||||
type Outfit = {
|
type Outfit = {
|
||||||
name: string,
|
name: string;
|
||||||
id: number
|
id: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export function useAvatarOutfits() {
|
export function useAvatarOutfits(): Outfit[] | false | null {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [outfits, setOutfits] = useState<Outfit[] | false | null>(null);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const query = useQuery<Outfit[] | false>({
|
||||||
|
queryKey: ["avatarOutfits", acct ? acct.id : "acctId"],
|
||||||
|
enabled: !!acct,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!acct) return false;
|
||||||
|
|
||||||
|
const res = await proxyFetch(
|
||||||
|
`https://avatar.roblox.com/v2/avatar/users/${acct.id}/outfits?page=1&itemsPerPage=25&isEditable=true`
|
||||||
|
);
|
||||||
|
const data = (await res.json()) as { data: Outfit[] };
|
||||||
|
|
||||||
|
loadThumbnails(
|
||||||
|
data.data.map((a) => ({
|
||||||
|
type: "Outfit",
|
||||||
|
targetId: a.id,
|
||||||
|
format: "webp",
|
||||||
|
size: "420x420"
|
||||||
|
}))
|
||||||
|
).catch(() => {});
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!acct) return;
|
|
||||||
|
|
||||||
let cancelled = false;
|
|
||||||
|
|
||||||
const fetchSetttings = async () => {
|
|
||||||
if (!acct || cancelled) return;
|
|
||||||
try {
|
|
||||||
const res = await proxyFetch(
|
|
||||||
`https://avatar.roblox.com/v2/avatar/users/${acct.id}/outfits?page=1&itemsPerPage=25&isEditable=true`
|
|
||||||
);
|
|
||||||
const data = await res.json() as {data: Outfit[]};
|
|
||||||
if (!cancelled) {
|
|
||||||
setOutfits(data.data);
|
|
||||||
loadThumbnails(data.data.map(a=>({
|
|
||||||
type: "Outfit",
|
|
||||||
targetId: a.id,
|
|
||||||
format: "webp",
|
|
||||||
size: "420x420"
|
|
||||||
}))).catch(a=>{})
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
if (!cancelled) setOutfits(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchSetttings();
|
|
||||||
|
|
||||||
const handleTransaction = () => {
|
const handleTransaction = () => {
|
||||||
fetchSetttings();
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["avatarOutfits", acct ? acct.id : "acctId"]
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("avatarTransactionCompletedEvent", handleTransaction);
|
window.addEventListener(
|
||||||
|
"avatarTransactionCompletedEvent",
|
||||||
|
handleTransaction
|
||||||
|
);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"avatarTransactionCompletedEvent",
|
"avatarTransactionCompletedEvent",
|
||||||
handleTransaction
|
handleTransaction
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [acct]);
|
}, [acct ? acct.id : "acctId", queryClient]);
|
||||||
|
|
||||||
return outfits;
|
return query.data ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,41 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// https://friends.roblox.com/v1/users/1083030325/friends/find?userSort=1
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
|
|
||||||
let isFetching = false;
|
export function useBestFriends():
|
||||||
let cachedData: any = null;
|
| {
|
||||||
|
hasVerifiedBadge: boolean;
|
||||||
export function useBestFriends() {
|
id: number;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
}[]
|
||||||
|
| null
|
||||||
|
| false {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [friends, setFriends] = useState<
|
|
||||||
|
const query = useQuery<
|
||||||
| {
|
| {
|
||||||
hasVerifiedBadge: boolean;
|
hasVerifiedBadge: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
}[]
|
}[]
|
||||||
| null
|
|
||||||
| false
|
| false
|
||||||
>(cachedData);
|
>({
|
||||||
|
queryKey: ["bestFriends", acct ? acct.id : "acctId"],
|
||||||
|
enabled: !!acct,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!acct) return false;
|
||||||
|
|
||||||
|
const BestFriendIDs = JSON.parse(
|
||||||
|
window.localStorage.getItem("BestFriendsStore") || "[]"
|
||||||
|
) as number[];
|
||||||
|
|
||||||
|
if (BestFriendIDs.length === 0) return [];
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (!acct) return;
|
|
||||||
if (isFetching) {
|
|
||||||
const IN = setInterval(() => {
|
|
||||||
if (cachedData !== null) {
|
|
||||||
if (!cancelled) setFriends(cachedData);
|
|
||||||
clearInterval(IN);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
return () => {
|
|
||||||
clearInterval(IN);
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
isFetching = true;
|
|
||||||
(async () => {
|
|
||||||
const BestFriendIDs = JSON.parse(window.localStorage.getItem("BestFriendsStore") || "[]") as number[]
|
|
||||||
const friendsAPICall2 = await proxyFetch(
|
const friendsAPICall2 = await proxyFetch(
|
||||||
`https://users.roblox.com/v1/users`,
|
`https://users.roblox.com/v1/users`,
|
||||||
{
|
{
|
||||||
@@ -51,6 +46,7 @@ export function useBestFriends() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const J2 = (await friendsAPICall2.json()) as {
|
const J2 = (await friendsAPICall2.json()) as {
|
||||||
data: {
|
data: {
|
||||||
hasVerifiedBadge: boolean;
|
hasVerifiedBadge: boolean;
|
||||||
@@ -59,6 +55,7 @@ export function useBestFriends() {
|
|||||||
displayName: string;
|
displayName: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
loadThumbnails(
|
loadThumbnails(
|
||||||
J2.data.map((a) => ({
|
J2.data.map((a) => ({
|
||||||
type: "AvatarHeadShot",
|
type: "AvatarHeadShot",
|
||||||
@@ -67,7 +64,8 @@ export function useBestFriends() {
|
|||||||
format: "webp"
|
format: "webp"
|
||||||
}))
|
}))
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
const friendsList = BestFriendIDs.map((a) => {
|
|
||||||
|
return BestFriendIDs.map((a) => {
|
||||||
const x = J2.data.find((b) => b.id === a);
|
const x = J2.data.find((b) => b.id === a);
|
||||||
return {
|
return {
|
||||||
id: a,
|
id: a,
|
||||||
@@ -76,14 +74,10 @@ export function useBestFriends() {
|
|||||||
displayName: x?.displayName || "?"
|
displayName: x?.displayName || "?"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (!cancelled) setFriends(friendsList);
|
},
|
||||||
cachedData = friendsList;
|
staleTime: 1000 * 60 * 5,
|
||||||
isFetching = false;
|
refetchOnWindowFocus: false
|
||||||
})();
|
});
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [acct]);
|
|
||||||
|
|
||||||
return friends;
|
return query.data ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,61 +1,36 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
getLoggedInUser,
|
getLoggedInUser,
|
||||||
getUserByUserId,
|
getUserByUserId,
|
||||||
UserProfileDetails
|
UserProfileDetails
|
||||||
} from "@/lib/profile";
|
} from "@/lib/profile";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
let isFetching = false;
|
export function useCurrentAccount(): UserProfileDetails | null | false {
|
||||||
let cachedData: UserProfileDetails | null | false = null;
|
const query = useQuery<UserProfileDetails | false>({
|
||||||
|
queryKey: ["currentAccount"],
|
||||||
export function useCurrentAccount() {
|
queryFn: async () => {
|
||||||
const [profileDetails, setProfileDetails] = useState<
|
|
||||||
UserProfileDetails | null | false
|
|
||||||
>(cachedData);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (profileDetails !== null && profileDetails !== undefined) return;
|
|
||||||
if (isFetching) {
|
|
||||||
const IN = setInterval(() => {
|
|
||||||
if (cachedData !== null) {
|
|
||||||
if (!cancelled) setProfileDetails(cachedData);
|
|
||||||
clearInterval(IN);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
return () => {
|
|
||||||
clearInterval(IN);
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
isFetching = true;
|
|
||||||
(async () => {
|
|
||||||
const authed = await getLoggedInUser();
|
const authed = await getLoggedInUser();
|
||||||
if (authed) {
|
if (!authed) return false;
|
||||||
const user = await getUserByUserId(authed.id.toString());
|
|
||||||
if (!cancelled) setProfileDetails(user);
|
|
||||||
cachedData = user;
|
|
||||||
loadThumbnails([
|
|
||||||
{
|
|
||||||
type: "AvatarHeadShot",
|
|
||||||
targetId: authed.id,
|
|
||||||
format: "webp",
|
|
||||||
size: "720x720"
|
|
||||||
}
|
|
||||||
]).catch(() => {});
|
|
||||||
} else {
|
|
||||||
if (!cancelled) setProfileDetails(false);
|
|
||||||
cachedData = false;
|
|
||||||
}
|
|
||||||
isFetching = false;
|
|
||||||
})();
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [profileDetails]);
|
|
||||||
|
|
||||||
return profileDetails;
|
const user = await getUserByUserId(authed.id.toString());
|
||||||
|
|
||||||
|
loadThumbnails([
|
||||||
|
{
|
||||||
|
type: "AvatarHeadShot",
|
||||||
|
targetId: authed.id,
|
||||||
|
format: "webp",
|
||||||
|
size: "720x720"
|
||||||
|
}
|
||||||
|
]).catch(() => {});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
},
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return query.data ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +1,34 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// https://friends.roblox.com/v1/users/1083030325/friends/find?userSort=1
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||||
|
import { UserProfileDetails } from "@/lib/profile";
|
||||||
let isFetching = false;
|
|
||||||
let cachedData: any = null;
|
|
||||||
|
|
||||||
export function useFriendsHome() {
|
export function useFriendsHome() {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [friends, setFriends] = useState<
|
const { data: friends } = useQuery({
|
||||||
| {
|
queryKey: ["friends", acct ? acct.id : "acctId"],
|
||||||
hasVerifiedBadge: boolean;
|
queryFn: async () => {
|
||||||
id: number;
|
if (!acct) return null;
|
||||||
name: string;
|
|
||||||
displayName: string;
|
|
||||||
}[]
|
|
||||||
| null
|
|
||||||
| false
|
|
||||||
>(cachedData);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let cancelled = false;
|
|
||||||
if (!acct) return;
|
|
||||||
if (isFetching) {
|
|
||||||
const IN = setInterval(() => {
|
|
||||||
if (cachedData !== null) {
|
|
||||||
if (!cancelled) setFriends(cachedData);
|
|
||||||
clearInterval(IN);
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
return () => {
|
|
||||||
clearInterval(IN);
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
isFetching = true;
|
|
||||||
(async () => {
|
|
||||||
const friendsAPICall = await proxyFetch(
|
const friendsAPICall = await proxyFetch(
|
||||||
`https://friends.roblox.com/v1/users/${acct.id}/friends` // /find?userSort=1
|
`https://friends.roblox.com/v1/users/${acct.id}/friends`
|
||||||
);
|
);
|
||||||
const J = (await friendsAPICall.json()) as {
|
const j = (await friendsAPICall.json()) as {
|
||||||
data: { id: number }[];
|
data: { id: number }[];
|
||||||
// PageItems: { id: number }[]; // /find
|
|
||||||
};
|
};
|
||||||
const friendsAPICall2 = await proxyFetch(
|
const friendsAPICall2 = await proxyFetch(
|
||||||
`https://users.roblox.com/v1/users`,
|
`https://users.roblox.com/v1/users`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
userIds: J.data.map((a) => a.id),
|
userIds: j.data.map((a) => a.id),
|
||||||
// userIds: J.PageItems.map((a) => a.id),
|
|
||||||
excludeBannedUsers: false
|
excludeBannedUsers: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const J2 = (await friendsAPICall2.json()) as {
|
const j2 = (await friendsAPICall2.json()) as {
|
||||||
data: {
|
data: {
|
||||||
hasVerifiedBadge: boolean;
|
hasVerifiedBadge: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
@@ -67,15 +37,15 @@ export function useFriendsHome() {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
loadThumbnails(
|
loadThumbnails(
|
||||||
J2.data.map((a) => ({
|
j2.data.map((a) => ({
|
||||||
type: "AvatarHeadShot",
|
type: "AvatarHeadShot",
|
||||||
size: "420x420",
|
size: "420x420",
|
||||||
targetId: a.id,
|
targetId: a.id,
|
||||||
format: "webp"
|
format: "webp"
|
||||||
}))
|
}))
|
||||||
).catch(() => {});
|
).catch(() => {});
|
||||||
const friendsList = J.data.map((a) => { // J.PageItems /find
|
const friendsList = j.data.map((a) => {
|
||||||
const x = J2.data.find((b) => b.id === a.id);
|
const x = j2.data.find((b) => b.id === a.id);
|
||||||
return {
|
return {
|
||||||
id: a.id,
|
id: a.id,
|
||||||
hasVerifiedBadge: x?.hasVerifiedBadge || false,
|
hasVerifiedBadge: x?.hasVerifiedBadge || false,
|
||||||
@@ -83,14 +53,14 @@ export function useFriendsHome() {
|
|||||||
displayName: x?.displayName || "?"
|
displayName: x?.displayName || "?"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
if (!cancelled) setFriends(friendsList);
|
return friendsList;
|
||||||
cachedData = friendsList;
|
},
|
||||||
isFetching = false;
|
enabled: !!acct,
|
||||||
})();
|
staleTime: 300000, // 5 minutes
|
||||||
return () => {
|
refetchOnWindowFocus: false,
|
||||||
cancelled = true;
|
refetchOnMount: false,
|
||||||
};
|
refetchOnReconnect: false
|
||||||
}, [acct]);
|
});
|
||||||
|
|
||||||
return friends;
|
return friends;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
// smartass method by google gemini
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { useEffect, useState, useMemo } from "react";
|
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -16,114 +14,53 @@ type PresenceData = {
|
|||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Internal Shared State ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Map to track subscribers.
|
|
||||||
* Key: The component's update callback function.
|
|
||||||
* Value: The array of user IDs that component is interested in.
|
|
||||||
* This allows multiple components to subscribe with their own lists of IDs.
|
|
||||||
*/
|
|
||||||
let subscribers = new Map<(data: PresenceData[]) => void, number[]>();
|
|
||||||
|
|
||||||
let interval: ReturnType<typeof setInterval> | null = null;
|
|
||||||
|
|
||||||
let latestData: PresenceData[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches presence for all unique user IDs requested by all subscribed components.
|
|
||||||
* @param acctId - The ID of the currently logged-in user.
|
|
||||||
*/
|
|
||||||
async function fetchPresence(acctId: number) {
|
|
||||||
const allIdArrays = [...subscribers.values()];
|
|
||||||
const uniqueUserIds = [...new Set(allIdArrays.flat())];
|
|
||||||
|
|
||||||
if (!acctId || uniqueUserIds.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await proxyFetch(
|
|
||||||
"https://presence.roblox.com/v1/presence/users",
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
userIds: [...new Set([acctId, ...uniqueUserIds])]
|
|
||||||
}),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.log(`[usePresence] API Error ${res.status} ${res.statusText}`)
|
|
||||||
return
|
|
||||||
// throw new Error(`API request failed with status ${res.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await res.json();
|
|
||||||
latestData = json.userPresences || [];
|
|
||||||
|
|
||||||
subscribers.forEach((_requestedIds, callback) => callback(latestData));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch presence:", error);
|
|
||||||
latestData = [];
|
|
||||||
subscribers.forEach((_requestedIds, callback) => callback([]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A React hook to get the real-time presence of a list of Roblox users.
|
* A React hook to get the real-time presence of a list of Roblox users.
|
||||||
* This hook can be used by multiple components simultaneously without conflict.
|
* This hook uses @tanstack/react-query to handle caching and periodic refetching.
|
||||||
*
|
*
|
||||||
* @param userIds - An array of user IDs to track.
|
* @param userIds - An array of user IDs to track.
|
||||||
* @returns An array of PresenceData objects for the requested user IDs.
|
* @returns An array of PresenceData objects for the requested user IDs.
|
||||||
*/
|
*/
|
||||||
export function useFriendsPresence(userIds: number[]) {
|
export function useFriendsPresence(userIds: number[]) {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [data, setData] = useState<PresenceData[]>([]);
|
|
||||||
|
|
||||||
const userIdsKey = useMemo(
|
// Sort userIds to ensure the query key is stable, regardless of the order of IDs.
|
||||||
() => JSON.stringify([...userIds].sort()),
|
const sortedUserIds = [...(userIds || [])].sort();
|
||||||
[userIds]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: presences = [] } = useQuery({
|
||||||
if (!acct || !userIds || userIds.length === 0) {
|
queryKey: ["presence", ...sortedUserIds],
|
||||||
setData([]);
|
queryFn: async () => {
|
||||||
return;
|
if (!acct || sortedUserIds.length === 0) {
|
||||||
}
|
return [];
|
||||||
|
|
||||||
const updateCallback = (globalData: PresenceData[]) => {
|
|
||||||
const filteredData = globalData.filter((presence) =>
|
|
||||||
userIds.includes(presence.userId)
|
|
||||||
);
|
|
||||||
setData(filteredData);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateCallback(latestData);
|
|
||||||
|
|
||||||
subscribers.set(updateCallback, userIds);
|
|
||||||
|
|
||||||
if (!interval) {
|
|
||||||
fetchPresence(acct.id);
|
|
||||||
interval = setInterval(() => fetchPresence(acct.id), 5000);
|
|
||||||
} else {
|
|
||||||
fetchPresence(acct.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The cleanup function runs when the component unmounts.
|
|
||||||
return () => {
|
|
||||||
subscribers.delete(updateCallback);
|
|
||||||
|
|
||||||
if (subscribers.size === 0 && interval) {
|
|
||||||
clearInterval(interval);
|
|
||||||
interval = null;
|
|
||||||
latestData = [];
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}, [acct, userIdsKey]);
|
|
||||||
|
|
||||||
return data;
|
const res = await proxyFetch(
|
||||||
|
"https://presence.roblox.com/v1/presence/users",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
userIds: sortedUserIds
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error(
|
||||||
|
`[usePresence] API Error ${res.status} ${res.statusText}`
|
||||||
|
);
|
||||||
|
throw new Error(`API request failed with status ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
return (json.userPresences || []) as PresenceData[];
|
||||||
|
},
|
||||||
|
enabled: !!acct && sortedUserIds.length > 0,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
refetchOnWindowFocus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return presences;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCurrentAccount } from "./useCurrentAccount";
|
import { useCurrentAccount } from "./useCurrentAccount";
|
||||||
import { proxyFetch } from "@/lib/utils";
|
import { proxyFetch } from "@/lib/utils";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
export function useRobuxBalance() {
|
export function useRobuxBalance() {
|
||||||
const acct = useCurrentAccount();
|
const acct = useCurrentAccount();
|
||||||
const [robux, setRobux] = useState<number | false | null>(null);
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: robux } = useQuery<number | false | null>({
|
||||||
if (!acct) return;
|
queryKey: ["robux-balance", acct ? acct.id : "acctId"],
|
||||||
|
queryFn: async () => {
|
||||||
let cancelled = false;
|
if (!acct) return null;
|
||||||
|
|
||||||
const fetchBalance = async () => {
|
|
||||||
if (!acct || cancelled) return;
|
|
||||||
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();
|
||||||
if (!cancelled) setRobux(data.robux);
|
return data.robux;
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setRobux(false);
|
return false;
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
enabled: !!acct,
|
||||||
fetchBalance();
|
refetchInterval: 10000,
|
||||||
const interval = setInterval(fetchBalance, 10000);
|
staleTime: 10000
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const handleTransaction = () => {
|
const handleTransaction = () => {
|
||||||
fetchBalance();
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["robux-balance", acct ? acct.id : "acctId"]
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("transactionCompletedEvent", handleTransaction);
|
window.addEventListener("transactionCompletedEvent", handleTransaction);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
|
||||||
clearInterval(interval);
|
|
||||||
window.removeEventListener(
|
window.removeEventListener(
|
||||||
"transactionCompletedEvent",
|
"transactionCompletedEvent",
|
||||||
handleTransaction
|
handleTransaction
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}, [acct]);
|
}, [acct ? acct.id : "acctId", queryClient]);
|
||||||
|
|
||||||
return robux;
|
return robux;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export function findClosestBrickColor(hex: string): {
|
|||||||
col: [number, number, number];
|
col: [number, number, number];
|
||||||
} {
|
} {
|
||||||
const target = hexToRgb(hex);
|
const target = hexToRgb(hex);
|
||||||
console.log(hex,target)
|
console.log(hex, target);
|
||||||
if (!target) throw new Error("Invalid hex");
|
if (!target) throw new Error("Invalid hex");
|
||||||
|
|
||||||
let bestDist = Infinity;
|
let bestDist = Infinity;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function proxyFetchRaw(
|
|||||||
...init,
|
...init,
|
||||||
method: init?.method || "GET",
|
method: init?.method || "GET",
|
||||||
headers,
|
headers,
|
||||||
body: init?.body,
|
body: init?.body
|
||||||
};
|
};
|
||||||
|
|
||||||
return window.fetch(proxyUrl, fetchInit);
|
return window.fetch(proxyUrl, fetchInit);
|
||||||
@@ -57,7 +57,7 @@ export async function proxyFetch(
|
|||||||
|
|
||||||
response = await proxyFetchRaw(input, {
|
response = await proxyFetchRaw(input, {
|
||||||
...init,
|
...init,
|
||||||
headers: newHeaders,
|
headers: newHeaders
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
if (!process.isBun) {
|
if (!process.isBun) {
|
||||||
console.error(`You are running this with node. Rerun the process: bun --bun run dev`)
|
console.error(
|
||||||
process.exit(1)
|
`You are running this with node. Rerun the process: bun --bun run dev`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.env.NEXT_PUBLIC_CWD = __dirname || "~"
|
process.env.NEXT_PUBLIC_CWD = __dirname || "~";
|
||||||
process.env.NEXT_PUBLIC_ARGV0 = process.argv0 || "node"
|
process.env.NEXT_PUBLIC_ARGV0 = process.argv0 || "node";
|
||||||
|
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
"@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/react-query": "^5.85.3",
|
||||||
|
"@tanstack/react-query-devtools": "^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