profile header n shit
This commit is contained in:
@@ -1,19 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { persistQueryClient } from "@tanstack/react-query-persist-client";
|
||||
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ReactQueryProvider({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [queryClient] = useState(() => new QueryClient());
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function ReactQueryProvider({ children }: Props) {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
retry: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Persist to localStorage (safe, runs client-side)
|
||||
const localStoragePersister = createAsyncStoragePersister({
|
||||
storage: window.localStorage
|
||||
});
|
||||
|
||||
persistQueryClient({
|
||||
queryClient,
|
||||
persister: localStoragePersister,
|
||||
maxAge: 1000 * 60 * 60 // 1 hour max
|
||||
});
|
||||
}, [window || "wtf"]);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} client={queryClient} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useMemo } from "react";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import { StupidHoverThing } from "../util/MiscStuff";
|
||||
import { VerifiedIcon } from "./RobloxIcons";
|
||||
import Link from "next/link";
|
||||
|
||||
export function FriendCarousel({
|
||||
friends: friendsUnsorted,
|
||||
@@ -122,7 +123,8 @@ export function FriendCarousel({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col min-w-[6.5rem]">
|
||||
<Link href={`/users/${a.id}`}>
|
||||
<div className="flex flex-col min-w-[6.5rem]">
|
||||
<LazyLoadedImage
|
||||
imgId={`AvatarHeadShot_${a.id}`}
|
||||
alt={a.name}
|
||||
@@ -141,6 +143,7 @@ export function FriendCarousel({
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</StupidHoverThing>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function FriendsHomeSect(
|
||||
) {
|
||||
const friends = useFriendsHome();
|
||||
|
||||
return <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||
return friends && <FriendCarousel {...props} title="Friends" friends={friends} />;
|
||||
}
|
||||
|
||||
export function BestFriendsHomeSect(
|
||||
|
||||
@@ -29,6 +29,8 @@ export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
|
||||
}
|
||||
alt={game.name}
|
||||
className="object-fill w-full h-full"
|
||||
lazyFetch={false} // ALWAYS fetch immediately
|
||||
size="384x216" // match game thumbnail size
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-muted">
|
||||
|
||||
117
components/roblox/UserProfileHeader.tsx
Normal file
117
components/roblox/UserProfileHeader.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import LazyLoadedImage from "../util/LazyLoadedImage";
|
||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||
import { OctagonXIcon } from "lucide-react";
|
||||
import {
|
||||
RobloxPremiumSmall,
|
||||
RobloxVerifiedSmall
|
||||
} from "@/components/roblox/RobloxTooltips";
|
||||
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||
import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
|
||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { UserProfileDetails } from "@/lib/profile";
|
||||
|
||||
export function UserProfileHeader({ user }: { user: UserProfileDetails }) {
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="justify-center w-screen px-8 py-6">
|
||||
<Alert variant="destructive" className="bg-base/50 space-x-2">
|
||||
<OctagonXIcon />
|
||||
<AlertTitle>Failed to fetch account info</AlertTitle>
|
||||
<AlertDescription>
|
||||
Is it a React Query bug?
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const presence = useFriendsPresence(user ? [user.id] : []);
|
||||
|
||||
const userActivity = presence.find((b) => b.userId === user?.id);
|
||||
const userPresence = userActivity?.userPresenceType;
|
||||
const borderColor =
|
||||
userPresence === 1
|
||||
? "border-blue/25 bg-blue/25"
|
||||
: userPresence === 2
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
|
||||
const isLoaded = !!user;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <button onClick={()=>console.log(userPresence)}>debug this</button> */}
|
||||
<div
|
||||
className="flex items-center gap-6 rounded-xl px-8 py-6 w-fit mt-8 ml-0"
|
||||
onContextMenu={(e) => {
|
||||
if (e.button === 2) {
|
||||
toast("[debug] reloading user pfp");
|
||||
console.log("[debug] reloading user pfp");
|
||||
loadThumbnails([
|
||||
{
|
||||
type: "AvatarHeadShot",
|
||||
targetId: user ? user.id : 1,
|
||||
format: "webp",
|
||||
size: "720x720"
|
||||
}
|
||||
]).catch(() => {});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!isLoaded ? (
|
||||
<Skeleton className="w-28 h-28 rounded-full" />
|
||||
) : (
|
||||
<LazyLoadedImage
|
||||
imgId={`AvatarHeadShot_${user.id}`}
|
||||
alt=""
|
||||
className={`w-28 h-28 rounded-full shadow-crust border-2 ${borderColor}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
||||
{isLoaded ? (
|
||||
<Link href={`/users/${user.id}`}>
|
||||
{user.displayName}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||
</>
|
||||
)}
|
||||
{isLoaded && user.hasVerifiedBadge ? (
|
||||
<RobloxVerifiedSmall className="w-6 h-6 fill-blue text-base" />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-base font-mono text-subtext0 mt-1">
|
||||
{isLoaded ? (
|
||||
<>
|
||||
@{user.name}
|
||||
{!!userActivity && userPresence === 2 ? (
|
||||
<> - {userActivity.lastLocation}</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Skeleton className="w-64 h-6 rounded-lg" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { useFriendsPresence } from "@/hooks/roblox/usePresence";
|
||||
import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
|
||||
import { loadThumbnails } from "@/lib/thumbnailLoader";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
|
||||
// chatgpt + human
|
||||
function randomGreeting(name: string): string {
|
||||
@@ -50,12 +51,12 @@ export function HomeLoggedInHeader() {
|
||||
userPresence === 1
|
||||
? "border-blue/25 bg-blue/25"
|
||||
: userPresence === 2
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
? "border-green/25 bg-green/25"
|
||||
: userPresence === 3
|
||||
? "border-yellow/25 bg-yellow/25"
|
||||
: userPresence === 0
|
||||
? "border-surface2/25 bg-surface2/25"
|
||||
: "border-red/25 bg-red/25";
|
||||
|
||||
const isLoaded = !!profile && !!accountSettings;
|
||||
|
||||
@@ -91,11 +92,13 @@ export function HomeLoggedInHeader() {
|
||||
<div className="flex flex-col justify-center">
|
||||
<span className="text-3xl font-bold text-text flex items-center gap-2">
|
||||
{isLoaded ? (
|
||||
randomGreeting(
|
||||
window.localStorage.UserPreferredName ||
|
||||
profile.displayName ||
|
||||
"Robloxian!"
|
||||
)
|
||||
<Link href={`/users/${profile.id}`}>
|
||||
{randomGreeting(
|
||||
window.localStorage.UserPreferredName ||
|
||||
profile.displayName ||
|
||||
"Robloxian!"
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<>
|
||||
<Skeleton className="w-96 h-8 rounded-lg" />
|
||||
|
||||
@@ -1,33 +1,78 @@
|
||||
import React from "react";
|
||||
import { useThumbnailURL } from "@/hooks/use-lazy-load";
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getThumbnails, ThumbnailRequest } from "@/lib/thumbnailLoader";
|
||||
import { Skeleton } from "../ui/skeleton";
|
||||
import Image from "next/image";
|
||||
|
||||
interface LazyLoadedImageProps {
|
||||
imgId: string;
|
||||
alt: string;
|
||||
[prop: string]: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
className?: string;
|
||||
lazyFetch?: boolean; // true for avatars, false for game thumbnails
|
||||
size?: string; // optional fetch size
|
||||
}
|
||||
|
||||
const LazyLoadedImage: React.FC<LazyLoadedImageProps> = ({
|
||||
imgId,
|
||||
alt,
|
||||
width = 1024,
|
||||
height = 1024,
|
||||
className,
|
||||
lazyFetch = true,
|
||||
size = "48x48",
|
||||
...props
|
||||
}) => {
|
||||
const imgUrl = useThumbnailURL(imgId);
|
||||
const [isVisible, setIsVisible] = useState(!lazyFetch);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [type, targetIdStr] = imgId.split("_");
|
||||
const targetId = Number(targetIdStr);
|
||||
|
||||
// React Query to fetch thumbnail when visible or immediately if lazyFetch=false
|
||||
const { data: thumbnails, isLoading } = useQuery({
|
||||
queryKey: ["thumbnails", targetId, size],
|
||||
queryFn: async () => {
|
||||
const result = await getThumbnails([
|
||||
{ type: type as any, targetId, format: "webp", size }
|
||||
]);
|
||||
return result;
|
||||
},
|
||||
enabled: isVisible,
|
||||
staleTime: 60_000 * 60 * 60 // 1 hour
|
||||
});
|
||||
|
||||
const imgUrl = thumbnails?.[0]?.imageUrl;
|
||||
|
||||
// IntersectionObserver only used if lazyFetch=true
|
||||
useEffect(() => {
|
||||
if (!lazyFetch || !ref.current) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => setIsVisible(entry.isIntersecting),
|
||||
{ rootMargin: "200px" }
|
||||
);
|
||||
|
||||
observer.observe(ref.current);
|
||||
return () => observer.disconnect();
|
||||
}, [lazyFetch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{imgUrl ? (
|
||||
<div ref={ref}>
|
||||
{imgUrl && !isLoading ? (
|
||||
<Image
|
||||
src={imgUrl as any}
|
||||
width={1024}
|
||||
height={1024}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton {...props} />
|
||||
<Skeleton className={className} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user