profile header n shit

This commit is contained in:
2025-08-15 16:15:28 +03:00
parent aec0052ef5
commit 6a1d81bfa8
17 changed files with 352 additions and 62 deletions

View File

@@ -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>
);
}

View File

@@ -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>
);
})}

View File

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

View File

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

View 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>
</>
);
}

View File

@@ -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" />

View File

@@ -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>
);