smart smart method by google gemini

This commit is contained in:
2025-07-24 06:25:14 +03:00
parent d1f925f298
commit 980b27bf84
14 changed files with 294 additions and 60 deletions

View File

@@ -2,7 +2,6 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { QuickTopUI } from "@/components/QuickTopUI";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -15,7 +14,7 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "OCbwoy3-Chan-blox", title: "ocbwoy3-chan-blox",
description: "roblox meets next.js i think" description: "roblox meets next.js i think"
}; };
@@ -27,10 +26,9 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden`}
> >
<TooltipProvider> <TooltipProvider>
<QuickTopUI />
{children} {children}
</TooltipProvider> </TooltipProvider>
</body> </body>

View File

@@ -3,6 +3,7 @@
import { FriendsHomeSect } from "@/components/FriendsOnlineSection"; import { FriendsHomeSect } from "@/components/FriendsOnlineSection";
import { GameCard } from "@/components/gameCard"; import { GameCard } from "@/components/gameCard";
import { HomeLoggedInHeader } from "@/components/loggedInHeader"; import { HomeLoggedInHeader } from "@/components/loggedInHeader";
import { QuickTopUI, QuickTopUILogoPart } from "@/components/QuickTopUI";
import { VerifiedIcon } from "@/components/RobloxIcons"; import { VerifiedIcon } from "@/components/RobloxIcons";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -35,7 +36,9 @@ export default function Home() {
}, []); }, []);
return ( return (
<div className="overflow-scroll no-scrollbar w-screen max-h-screen h-screen"> <div className="overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
<QuickTopUI />
<QuickTopUILogoPart />
<HomeLoggedInHeader /> <HomeLoggedInHeader />
<FriendsHomeSect className="pt-8" /> <FriendsHomeSect className="pt-8" />
<div className="justify-center w-screen px-8 pt-6"> <div className="justify-center w-screen px-8 pt-6">

View File

@@ -33,6 +33,7 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@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",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
@@ -275,6 +276,8 @@
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@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=="],
"@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="], "@types/d3-array": ["@types/d3-array@3.2.1", "", {}, "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg=="],
"@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],

View File

@@ -3,6 +3,8 @@ import { useFriendsHome } from "@/hooks/roblox/useFriendsHome";
import LazyLoadedImage from "./lazyLoadedImage"; import LazyLoadedImage from "./lazyLoadedImage";
import React from "react"; import React from "react";
import { VerifiedIcon } from "./RobloxIcons"; import { VerifiedIcon } from "./RobloxIcons";
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
import { StupidHoverThing } from "./MiscStuff";
export function FriendsHomeSect( export function FriendsHomeSect(
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
@@ -12,6 +14,9 @@ export function FriendsHomeSect(
) { ) {
const friends = useFriendsHome(); const friends = useFriendsHome();
const acct = useCurrentAccount(); const acct = useCurrentAccount();
const presence = useFriendsPresence(
(!!friends ? friends : []).map((f) => f.id)
);
if (!friends) { if (!friends) {
return <></>; return <></>;
@@ -19,35 +24,93 @@ export function FriendsHomeSect(
return ( return (
<div {...props}> <div {...props}>
{/* <button onClick={()=>console.log(acct,presence,friends)}>debug</button> */}
<h1 className="text-2xl pb-2 pl-4">Friends</h1> <h1 className="text-2xl pb-2 pl-4">Friends</h1>
<div className="bg-base p-4 rounded-xl flex flex-col gap-2 px-4"> <div className="bg-base p-4 rounded-xl flex flex-col gap-2 px-4 no-scrollbar">
<div <div
className="flex items-center gap-4 overflow-x-auto pb-2 -mx-4 w-screen scrollbar-thin scrollbar-thumb-surface2 scrollbar-track-surface0" className="flex 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"
style={{ style={{
scrollSnapType: "x mandatory", scrollSnapType: "x mandatory",
WebkitOverflowScrolling: "touch" WebkitOverflowScrolling: "touch",
scrollbarWidth: "none"
}} }}
> >
<div className="w-8" /> <div className="w-8" />
{friends.map((a) => ( {friends.map((a) => {
<div const userStatus = presence.find(
key={a.id} (b) => b.userId === a.id
className="flex flex-col items-center min-w-[6.5rem]" );
// style={{ scrollSnapAlign: "start" }} const userPresence = userStatus?.userPresenceType || 0;
> const borderColor =
<LazyLoadedImage userPresence === 1
imgId={`AvatarHeadShot_${a.id}`} ? "border-blue bg-blue/50"
alt={a.name} : userPresence === 2
className="w-24 h-24 rounded-full border-2 border-surface2 object-cover shadow" ? "border-green bg-green/50"
/> : userPresence === 3
<span className="truncate text-xs text-text mt-1 text-center flex items-center justify-center gap-1"> ? "border-yellow bg-yellow/50"
{a.displayName || a.name} : userPresence === 0
{a.hasVerifiedBadge ? null : ( ? "border-surface2 bg-surface2/50"
<VerifiedIcon className="text-base fill-blue w-3 h-3" /> : "border-red bg-red/50";
)} const textColor =
</span> userPresence === 1
</div> ? "text-blue"
))} : userPresence === 2
? "text-green"
: userPresence === 3
? "text-yellow"
: userPresence === 0
? "text-surface2"
: "text-red";
const fillColor =
userPresence === 1
? "fill-blue"
: userPresence === 2
? "fill-green"
: userPresence === 3
? "fill-yellow"
: userPresence === 0
? "fill-surface2"
: "fill-red";
return (
<div
key={a.id}
className="flex flex-col min-w-[6.5rem]"
>
<LazyLoadedImage
imgId={`AvatarHeadShot_${a.id}`}
alt={a.name}
className={`w-24 h-24 rounded-full border-2 ${borderColor} object-cover shadow-xl`}
/>
<span
className={`text-xs ${textColor} mt-1 text-center flex items-center justify-center gap-1 max-w-[6.5rem] overflow-hidden line-clamp-2`}
>
<StupidHoverThing
text={
<span className="space-x-1 flex items-center">
<p>{a.displayName || a.name}</p>
{!a.hasVerifiedBadge ? (
<VerifiedIcon
useDefault
className={`w-4 h-4 shrink-0`}
/>
) : null}
</span>
}
>
<span className="line-clamp-1 overflow-hidden text-ellipsis">
{a.displayName || a.name}
</span>
</StupidHoverThing>
{!a.hasVerifiedBadge ? (
<VerifiedIcon
className={`text-base ${fillColor} w-3 h-3 shrink-0`}
/>
) : null}
</span>
</div>
);
})}
<div className="w-8" /> <div className="w-8" />
</div> </div>
</div> </div>

15
components/MiscStuff.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { VerifiedIcon } from "./RobloxIcons";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
export function StupidHoverThing({ children, text }: React.PropsWithChildren & { text: string | React.ReactNode }) {
return (
<Tooltip>
<TooltipTrigger asChild>
{children}
</TooltipTrigger>
<TooltipContent className="bg-surface0 text-text m-2">
<p className="text-sm flex items-center">{text}</p>
</TooltipContent>
</Tooltip>
)
}

View File

@@ -2,18 +2,27 @@
import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance"; import { useRobuxBalance } from "@/hooks/roblox/useRobuxBalance";
import { RobuxIcon } from "./RobloxIcons"; import { RobuxIcon } from "./RobloxIcons";
import React from "react";
export function QuickTopUI() { export const QuickTopUI = React.memo(function () {
const robux = useRobuxBalance(); const robux = useRobuxBalance();
return ( return (
<span className="z-50 absolute top-4 right-4 p-4 flex gap-4 items-center"> <div className="z-50 absolute top-4 right-4 p-4 flex gap-4 items-center">
<img src="/icon-128.webp" className="-m-1 w-8 h-8" alt="" /> <div className="rounded-full bg-crust/50 flex items-center p-2">
<span className="rounded-full bg-crust/50 flex items-center p-2"> <div className="px-2 font-sans text-blue text-xl flex items-center">
<span className="px-2 font-sans text-blue text-xl flex items-center">
<RobuxIcon className="w-6 h-6" /> <RobuxIcon className="w-6 h-6" />
<p className="pl-1">{robux}</p> <p className="pl-1">{robux || "???"}</p>
</span> </div>
</span> </div>
</span> </div>
); );
} });
export const QuickTopUILogoPart = React.memo(function () {
return (
<div className="z-50 relative top-4 left-4 p-4 flex gap-4 items-center text-blue">
<img src="/icon-128.webp" className="-m-1 w-8 h-8" alt="" />
<p className="mt-2">{"ocbwoy3-chan-blox"}</p>
</div>
);
});

View File

@@ -28,7 +28,7 @@ export function RobloxVerifiedSmall(
<VerifiedIcon {...props} /> <VerifiedIcon {...props} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="bg-surface0 text-text m-2"> <TooltipContent className="bg-surface0 text-text m-2">
<p className="text-sm">Verified user</p> <p className="text-sm">Verified</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
); );

View File

@@ -1,12 +1,13 @@
"use client"; "use client";
import React from "react"; import React, { useEffect } from "react";
import LazyLoadedImage from "./lazyLoadedImage"; import LazyLoadedImage from "./lazyLoadedImage";
import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { Alert, AlertDescription, AlertTitle } from "./ui/alert";
import { OctagonXIcon } from "lucide-react"; import { OctagonXIcon } from "lucide-react";
import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff"; import { RobloxPremiumSmall, RobloxVerifiedSmall } from "./RobloxTooltipStuff";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount"; import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { Skeleton } from "./ui/skeleton"; import { Skeleton } from "./ui/skeleton";
import { useFriendsPresence } from "@/hooks/roblox/usePresence";
export function HomeLoggedInHeader() { export function HomeLoggedInHeader() {
const profile = useCurrentAccount(); const profile = useCurrentAccount();
@@ -26,8 +27,24 @@ export function HomeLoggedInHeader() {
); );
} }
const presence = useFriendsPresence(profile ? [profile.id] : []);
const userActivity = presence.find((b) => b.userId === profile?.id)
const userPresence = userActivity?.userPresenceType;
const borderColor =
userPresence === 1
? "border-blue bg-blue/50"
: userPresence === 2
? "border-green bg-green/50"
: userPresence === 3
? "border-yellow bg-yellow/50"
: userPresence === 0
? "border-surface2 bg-surface2/50"
: "border-red bg-red/50";
return ( return (
<> <>
{/* <button onClick={()=>console.log(userPresence)}>debug this</button> */}
<div className="flex items-center gap-6 bg-base rounded-xl px-8 py-6 w-fit mt-8 ml-0"> <div className="flex items-center gap-6 bg-base rounded-xl px-8 py-6 w-fit mt-8 ml-0">
{!profile ? ( {!profile ? (
<Skeleton className="w-28 h-28 rounded-full" /> <Skeleton className="w-28 h-28 rounded-full" />
@@ -35,7 +52,7 @@ export function HomeLoggedInHeader() {
<LazyLoadedImage <LazyLoadedImage
imgId={`AvatarHeadShot_${profile.id}`} imgId={`AvatarHeadShot_${profile.id}`}
alt="" alt=""
className="w-28 h-28 rounded-full shadow-crust" className={`w-28 h-28 rounded-full shadow-crust border-2 ${borderColor}`}
/> />
)} )}
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center">
@@ -53,7 +70,10 @@ export function HomeLoggedInHeader() {
</span> </span>
<span className="text-base font-mono text-subtext0 mt-1"> <span className="text-base font-mono text-subtext0 mt-1">
{profile ? ( {profile ? (
<>@{profile.name}</> <>
@{profile.name}
{(!!userActivity && userPresence === 2) ? <> - {userActivity.lastLocation}</> : <></> }
</>
) : ( ) : (
<Skeleton className="w-64 h-6 rounded-lg" /> <Skeleton className="w-64 h-6 rounded-lg" />
)} )}

View File

@@ -9,7 +9,7 @@ import { loadThumbnails } from "@/lib/thumbnailLoader";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
let isFetching = false; let isFetching = false;
let cachedData: any = null; let cachedData: UserProfileDetails | null | false = null;
export function useCurrentAccount() { export function useCurrentAccount() {
const [profileDetails, setProfileDetails] = useState< const [profileDetails, setProfileDetails] = useState<
@@ -18,7 +18,7 @@ export function useCurrentAccount() {
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
if (profileDetails !== null) return; if (profileDetails !== null && profileDetails !== undefined) return;
if (isFetching) { if (isFetching) {
const IN = setInterval(() => { const IN = setInterval(() => {
if (cachedData !== null) { if (cachedData !== null) {

127
hooks/roblox/usePresence.ts Normal file
View File

@@ -0,0 +1,127 @@
"use client";
// smartass method by google gemini
import { useEffect, useState, useMemo } from "react";
import { useCurrentAccount } from "./useCurrentAccount";
import { proxyFetch } from "@/lib/utils";
type PresenceData = {
userPresenceType: number;
lastLocation: string;
placeId: number;
rootPlaceId: number;
gameId: string;
universeId: 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) {
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.
* This hook can be used by multiple components simultaneously without conflict.
*
* @param userIds - An array of user IDs to track.
* @returns An array of PresenceData objects for the requested user IDs.
*/
export function useFriendsPresence(userIds: number[]) {
const acct = useCurrentAccount();
const [data, setData] = useState<PresenceData[]>([]);
const userIdsKey = useMemo(
() => JSON.stringify([...userIds].sort()),
[userIds]
);
useEffect(() => {
if (!acct || !userIds || userIds.length === 0) {
setData([]);
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;
}

View File

@@ -4,46 +4,40 @@ import { useEffect, useState } from "react";
import { useCurrentAccount } from "./useCurrentAccount"; import { useCurrentAccount } from "./useCurrentAccount";
import { proxyFetch } from "@/lib/utils"; import { proxyFetch } from "@/lib/utils";
let isFetching = false;
let cachedData: number | false | null = null;
export function useRobuxBalance() { export function useRobuxBalance() {
const acct = useCurrentAccount(); const acct = useCurrentAccount();
const [robux, setRobux] = useState<number | false | null>(cachedData); const [robux, setRobux] = useState<number | false | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false;
if (!acct) return; if (!acct) return;
async function fetchBalance() { let cancelled = false;
if (isFetching) return;
if (!acct) return; const fetchBalance = async () => {
isFetching = true; 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); if (!cancelled) setRobux(data.robux);
cachedData = data.robux;
} catch { } catch {
if (!cancelled) setRobux(false); if (!cancelled) setRobux(false);
cachedData = false;
} finally {
isFetching = false;
} }
} };
fetchBalance(); fetchBalance();
const interval = setInterval(fetchBalance, 10000);
function handleTransaction() { const handleTransaction = () => {
fetchBalance(); fetchBalance();
} };
window.addEventListener("transactionCompletedEvent", handleTransaction); window.addEventListener("transactionCompletedEvent", handleTransaction);
return () => { return () => {
cancelled = true; cancelled = true;
clearInterval(interval);
window.removeEventListener( window.removeEventListener(
"transactionCompletedEvent", "transactionCompletedEvent",
handleTransaction handleTransaction

View File

@@ -26,7 +26,7 @@ export type ThumbnailRequest = {
export async function getThumbnails( export async function getThumbnails(
b: ThumbnailRequest[] b: ThumbnailRequest[]
): Promise<AssetThumbnail[]> { ): Promise<AssetThumbnail[]> {
const batchSize = 50; 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);

View File

@@ -38,6 +38,7 @@
"@radix-ui/react-toggle": "^1.1.9", "@radix-ui/react-toggle": "^1.1.9",
"@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",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",

View File

@@ -115,6 +115,7 @@ export default {
}, },
plugins: [ plugins: [
require("tailwindcss-animate"), require("tailwindcss-animate"),
require("@tailwindcss/line-clamp"),
require("@catppuccin/tailwindcss")({ require("@catppuccin/tailwindcss")({
prefix: false, prefix: false,
defaultFlavour: "mocha" defaultFlavour: "mocha"