fixxxxxxxx

This commit is contained in:
2025-12-27 14:20:22 +02:00
parent 3612ada03a
commit 5bfdd7dd2b
26 changed files with 905 additions and 626 deletions

View File

@@ -5,6 +5,7 @@ import Link from "next/link";
import { usePlaceDetails } from "@/hooks/roblox/usePlaceDetails";
import { RobloxVerifiedSmall } from "@/components/roblox/RobloxTooltips";
import { Button } from "@/components/ui/button";
import { useGameLaunch } from "@/components/providers/GameLaunchProvider";
interface GamePageContentProps {
placeId: string;
@@ -12,11 +13,12 @@ interface GamePageContentProps {
export default function GamePageContent({ placeId }: GamePageContentProps) {
const game = usePlaceDetails(placeId);
const { launchGame } = useGameLaunch();
// Set dynamic document title
useEffect(() => {
if (!!game) {
document.title = `${game.name} | ocbwoy3-chan's roblox`;
document.title = `${game.name} | Roblox`;
}
}, [game]);
@@ -24,7 +26,7 @@ export default function GamePageContent({ placeId }: GamePageContentProps) {
return (
<div className="p-4 space-y-6">
<Button onClick={a=>open(`roblox://placeId=${game.rootPlaceId}`)}>
<Button onClick={() => launchGame(game.rootPlaceId.toString())}>
PLAY
</Button>
<div className="break-all pl-4 whitespace-pre-line font-black text-2xl">

View File

@@ -1,6 +1,5 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.ts";
@import "tailwindcss";
body {
font-family: SF Pro Display, Geist;
@@ -12,6 +11,20 @@ body {
@layer base {
:root {
--ctp-base: 240 21.052631735801697% 14.901961386203766%; /* base */
--ctp-mantle: 240 21.311475336551666% 11.96078434586525%; /* mantle */
--ctp-crust: 240 23.404255509376526% 8.627450853586197%; /* crust */
--ctp-text: 226 63.93442749977112% 88.03921341896057%; /* text */
--ctp-subtext0: 227 23.076922595500946% 71.96078300476074%; /* subtext0 */
--ctp-subtext1: 227 35.29411852359772% 80.0000011920929%; /* subtext1 */
--ctp-surface0: 237 16.239316761493683% 22.94117659330368%; /* surface0 */
--ctp-surface1: 234 13.20754736661911% 31.176471710205078%; /* surface1 */
--ctp-surface2: 233 12.05937068939209% 39.607844948768616%; /* surface2 */
--ctp-blue: 217 91.86992049217224% 75.88235139846802%; /* blue */
--ctp-green: 115 54.09836173057556% 76.07843279838562%; /* green */
--ctp-yellow: 41 86.04651093482971% 83.13725590705872%; /* yellow */
--ctp-red: 343 81.25% 74.90196228027344%; /* red */
--background: 240 21.052631735801697% 14.901961386203766%; /* base */
--foreground: 226 63.93442749977112% 88.03921341896057%; /* text */
@@ -69,6 +82,44 @@ body {
}
}
@theme {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar-background));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-ring: hsl(var(--sidebar-ring));
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
}
@layer base {
* {
@apply border-border;
@@ -88,3 +139,6 @@ body {
scrollbar-width: 0;
}
}
@utility border-border {
border-color: hsl(var(--border));
}

View File

@@ -6,6 +6,8 @@ import { Toaster } from "@/components/ui/toaster";
import Image from "next/image";
import { QuickTopUI, QuickTopUILogoPart } from "@/components/site/QuickTopUI";
import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider";
import { GameLaunchProvider } from "@/components/providers/GameLaunchProvider";
import { GameLaunchDialog } from "@/components/providers/GameLaunchDialog";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -18,8 +20,10 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "home | ocbwoy3-chan's roblox",
description: "roblox meets next.js i think"
title: "Home | Roblox",
description: "Roblox is a global platform that brings people together through play.",
authors: [{name: "Roblox Corporation"}],
keywords: ["free games", "online games", "building games", "virtual worlds", "free mmo", "gaming cloud", "physics engine"]
};
export default function RootLayout({
@@ -34,6 +38,7 @@ export default function RootLayout({
>
<ReactQueryProvider>
<TooltipProvider>
<GameLaunchProvider>
<main>
{/* <Image
src={"/bg.png"}
@@ -48,7 +53,9 @@ export default function RootLayout({
{children}
</div>
</main>
<GameLaunchDialog />
<Toaster />
</GameLaunchProvider>
</TooltipProvider>
</ReactQueryProvider>
</body>

View File

@@ -55,7 +55,7 @@ export default function Home() {
<div className="h-4" />
<BestFriendsHomeSect className="pt-2" />
<FriendsHomeSect className="pt-2" />
<div className="justify-center w-screen px-8 pt-6">
{/* <div className="justify-center w-screen px-8 pt-6">
<Alert variant="default" className="bg-base/50 space-x-2">
<AlertTriangleIcon />
<AlertTitle>Warning</AlertTitle>
@@ -64,7 +64,7 @@ export default function Home() {
process on GitHub.
</AlertDescription>
</Alert>
</div>
</div> */}
<div className="p-4 space-y-8 no-scrollbar">
{isLoading || !rec ? (

View File

@@ -42,7 +42,7 @@ export default function UserProfileContent({
// Set dynamic document title
useEffect(() => {
if (profile?.displayName) {
document.title = `${profile.displayName}'s profile | ocbwoy3-chan's roblox`;
document.title = `${profile.displayName}'s profile | Roblox`;
}
}, [profile]);

669
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
"use client";
import { useEffect, useState, useSyncExternalStore } from "react";
import { closeGameLaunch, getGameLaunchState, subscribeGameLaunch } from "@/components/providers/game-launch-store";
import { Button } from "@/components/ui/button";
import { X } from "lucide-react";
import { RobloxLogoIcon } from "@/components/roblox/RobloxIcons";
import Link from "next/link";
export function GameLaunchDialog() {
const state = useSyncExternalStore(
subscribeGameLaunch,
getGameLaunchState,
getGameLaunchState
);
const [launchTimeouted, setLaunchTimeouted] = useState<boolean>(false);
useEffect(() => {
if (!state.isOpen) {
setLaunchTimeouted(false);
return;
}
const timeout = setTimeout(() => {
setLaunchTimeouted(true);
}, 5000); // 5 seconds
return () => clearTimeout(timeout);
}, [state.isOpen]);
if (!state.isOpen) return null;
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-mantle/70 backdrop-blur-sm"
onClick={closeGameLaunch}
>
<div
className="relative w-[92vw] max-w-sm rounded-2xl bg-crust/95 ring-1 ring-surface0/60 shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<Button
variant="ghost"
size="icon"
onClick={closeGameLaunch}
aria-label="Close launcher"
className="absolute right-3 top-3"
>
<X className="h-4 w-4" />
</Button>
<div className="flex flex-col items-center gap-4 px-6 py-8 text-center">
<div className="h-24 w-24 flex items-center justify-center">
<RobloxLogoIcon />
</div>
<div className="space-y-1">
<p className="text-2xl font-semibold text-text">
{!launchTimeouted ? (
<>
Roblox is now loading.<br />Get Ready!
</>
) : (
<>Download Roblox to play millions of experiences!</>
)}
</p>
</div>
<Button disabled={!launchTimeouted} variant="default" className="w-full rounded-full">
{launchTimeouted ? (
<Link href="https://flathub.org/en/apps/org.vinegarhq.Sober" target="_blank" rel="noopener noreferrer">
Download Roblox
</Link>
) : null}
{!launchTimeouted && <div className="h-4 w-4 rounded-full border-2 border-white/70 border-t-transparent animate-spin" />}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,60 @@
"use client";
import React, { createContext, useCallback, useContext, useMemo } from "react";
import { openGameLaunchWithParams } from "@/components/providers/game-launch-store";
type GameLaunchContextValue = {
launchGame: (placeId: string, jobId?: string) => void;
};
const GameLaunchContext = createContext<GameLaunchContextValue | null>(null);
export function useGameLaunch() {
const ctx = useContext(GameLaunchContext);
if (!ctx) {
throw new Error("useGameLaunch must be used within GameLaunchProvider");
}
return ctx;
}
export function GameLaunchProvider({
children
}: {
children: React.ReactNode;
}) {
const launchGame = useCallback((placeId: string, jobId?: string) => {
openGameLaunchWithParams(placeId, jobId);
console.log("[GameLaunchProvider] Launching",{placeId, jobId});
const gameLaunchParams = {
launchmode: "play",
LaunchExp: "InApp",
placeId: placeId,
gameInstanceId: jobId ?? undefined
};
console.log("[GameLaunchProvider] Constructed GameLaunchParams",gameLaunchParams);
const url = new URL("roblox://experiences/start")
for (const [key, value] of Object.entries(gameLaunchParams)) {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
}
const deeplinkNew = url.toString();
console.log("[GameLaunchProvider] Opening URL:", deeplinkNew);
document.location.href = deeplinkNew;
}, []);
const value = useMemo(() => ({ launchGame }), [launchGame]);
return (
<GameLaunchContext.Provider value={value}>
{children}
</GameLaunchContext.Provider>
);
}

View File

@@ -1,6 +1,6 @@
"use client";
import { ReactNode, useEffect } from "react";
import { ReactNode, useEffect, useState } 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";
@@ -11,19 +11,22 @@ interface Props {
}
export function ReactQueryProvider({ children }: Props) {
const queryClient = new QueryClient({
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: true
}
}
});
})
);
// will cause bun to SEGFAULT
useEffect(() => {
if (!window) return;
if (typeof window === "undefined") return;
// Persist to localStorage (safe, runs client-side)
const localStoragePersister = createAsyncStoragePersister({
storage: window.localStorage
@@ -34,7 +37,7 @@ export function ReactQueryProvider({ children }: Props) {
persister: localStoragePersister,
maxAge: 1000 * 60 * 60 // 1 hour max
});
}, [window || "wtf"]);
}, [queryClient]);
return (
<QueryClientProvider client={queryClient}>

View File

@@ -0,0 +1,35 @@
type GameLaunchState = {
isOpen: boolean;
placeId?: string;
gameInstanceId?: string;
};
let state: GameLaunchState = { isOpen: false };
const listeners = new Set<() => void>();
function emit() {
listeners.forEach((listener) => listener());
}
export function getGameLaunchState() {
return state;
}
export function subscribeGameLaunch(listener: () => void) {
listeners.add(listener);
return () => listeners.delete(listener);
}
export function openGameLaunchWithParams(placeId: string, jobId?: string) {
state = {
isOpen: true,
placeId,
gameInstanceId: jobId
};
emit();
}
export function closeGameLaunch() {
state = { isOpen: false };
emit();
}

View File

@@ -11,12 +11,15 @@ import {
import { ContextMenuItem } from "@radix-ui/react-context-menu";
import React from "react";
import Link from "next/link";
import { useGameLaunch } from "@/components/providers/GameLaunchProvider";
interface GameCardProps {
game: ContentMetadata;
}
export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
const { launchGame } = useGameLaunch();
return (
<ContextMenu>
<ContextMenuTrigger>
@@ -78,7 +81,7 @@ export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
</ContextMenuItem>
<ContextMenuItem
onClick={() => {
window.location.href = `roblox://placeId=${game.rootPlaceId}`;
launchGame(game.rootPlaceId.toString());
}}
>
Play

View File

@@ -95,3 +95,21 @@ export const RobuxIcon = (props: React.SVGProps<SVGSVGElement>) => (
</g>
</svg>
);
export const RobloxLogoIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" fill="none" viewBox="0 0 1024 1024">
<g clipPath="url(#a)">
<mask id="b" width="1024" height="1024" x="0" y="0" maskUnits="userSpaceOnUse" className="mask-type-alpha">
<path fill="#d9d9d9" d="M0 365.856c0-128.061 0-192.092 24.923-241.005a228.66 228.66 0 0 1 99.928-99.928C173.764 0 237.795 0 365.856 0h292.288c128.061 0 192.092 0 241.005 24.923a228.66 228.66 0 0 1 99.929 99.928C1024 173.764 1024 237.795 1024 365.856v292.288c0 128.061 0 192.092-24.922 241.005a228.66 228.66 0 0 1-99.929 99.929C850.236 1024 786.205 1024 658.144 1024H365.856c-128.061 0-192.092 0-241.005-24.922a228.66 228.66 0 0 1-99.928-99.929C0 850.236 0 786.205 0 658.144z"/>
</mask>
<g mask="url(#b)"><path fill="#335fff" d="M0 0h1024v1024H0z"/>
<path fill="#fff" d="m307.201 157.281-149.92 559.518 559.518 149.92 149.92-559.518zm262.041 453.876-156.349-41.915 41.914-156.349 156.412 41.914z"/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h1024v1024H0z"/>
</clipPath>
</defs>
</svg>
)

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import LazyLoadedImage from "../util/LazyLoadedImage";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { OctagonXIcon } from "lucide-react";
@@ -15,6 +15,7 @@ import { useAccountSettings } from "@/hooks/roblox/useAccountSettings";
import { loadThumbnails } from "@/lib/thumbnailLoader";
import { toast } from "sonner";
import Link from "next/link";
import { Button } from "../ui/button";
// chatgpt + human
function randomGreeting(name: string): string {
@@ -27,6 +28,15 @@ function randomGreeting(name: string): string {
export function HomeLoggedInHeader() {
const profile = useCurrentAccount();
const accountSettings = useAccountSettings();
const [preferredName, setPreferredName] = useState<string | null>(null);
const profileId = profile ? profile.id : undefined;
const presence = useFriendsPresence(profileId ? [profileId] : []);
useEffect(() => {
if (typeof window === "undefined") return;
const storedName = window.localStorage.getItem("UserPreferredName");
if (storedName) setPreferredName(storedName);
}, []);
if (profile === false) {
return (
@@ -43,8 +53,6 @@ export function HomeLoggedInHeader() {
);
}
const presence = useFriendsPresence(profile ? [profile.id] : []);
const userActivity = presence.find((b) => b.userId === profile?.id);
const userPresence = userActivity?.userPresenceType;
const borderColor =
@@ -94,7 +102,7 @@ export function HomeLoggedInHeader() {
{isLoaded ? (
<Link href={`/users/${profile.id}`}>
{randomGreeting(
window.localStorage.UserPreferredName ||
preferredName ||
profile.displayName ||
"Robloxian!"
)}

View File

@@ -2,12 +2,12 @@
import { useAvatarOutfits } from "@/hooks/roblox/useAvatarOutfits";
import { Button } from "@/components/ui/button";
import { cn, proxyFetch } from "@/lib/utils";
import LazyLoadedImage from "../util/LazyLoadedImage";
import { StupidHoverThing } from "../util/MiscStuff";
import { loadThumbnails } from "@/lib/thumbnailLoader";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { useEffect } from "react";
import { X } from "lucide-react";
type OutfitSelectorProps = {
setVisible: (visible: boolean) => void;
@@ -25,7 +25,7 @@ export function OutfitSelector({
const acc = useCurrentAccount();
useEffect(() => {
if (!outfits) return;
if (!outfits || outfits.length === 0) return;
loadThumbnails(
outfits.map((a) => ({
type: "Outfit",
@@ -34,38 +34,100 @@ export function OutfitSelector({
size: "420x420"
}))
).catch(() => {});
}, [acc, outfits]);
}, [outfits]);
if (!outfits || !acc) return null;
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") setVisible(false);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [setVisible]);
const isLoading = outfits === null;
const hasOutfits = Array.isArray(outfits) && outfits.length > 0;
return (
<div className="z-30 isolate absolute inset-0 flex items-center justify-center bg-crust/50">
<button
className="z-10 absolute w-screen h-screen cursor-default"
onClick={() => {
setVisible(false);
}}
/>
<div className="z-20 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 p-8 bg-crust/90 rounded-xl">
{(outfits || []).map((outfit: { id: number; name: string }) => (
<StupidHoverThing key={outfit.id} delayDuration={0} text={outfit.name}>
<button
<div
className="fixed inset-0 z-40 flex items-center justify-center bg-mantle/70 backdrop-blur-sm"
onClick={() => setVisible(false)}
>
<div
className="relative w-full max-w-3xl sm:max-w-4xl mx-4 rounded-2xl bg-crust/95 ring-1 ring-surface0/60 shadow-2xl"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-center justify-between border-b border-surface0/60 px-6 py-4">
<div>
<p className="text-lg font-semibold text-text">Outfits</p>
<p className="text-xs text-subtext1">
Pick a look to update your avatar instantly.
</p>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => setVisible(false)}
aria-label="Close outfit chooser"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="p-6">
{!acc ? (
<div className="rounded-xl border border-surface0/60 bg-base/50 p-6 text-sm text-subtext1">
Sign in to load your outfits.
</div>
) : isLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{Array.from({ length: 8 }).map((_, index) => (
<div
key={`outfit-skeleton-${index}`}
className="rounded-xl border border-surface0/60 bg-base/40 p-3"
>
<div className="h-24 w-24 sm:h-28 sm:w-28 rounded-lg bg-surface0/70 animate-pulse" />
<div className="mt-3 h-3 w-20 rounded bg-surface0/70 animate-pulse" />
</div>
))}
</div>
) : hasOutfits ? (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 max-h-[60vh] overflow-y-auto pr-2">
{outfits.map((outfit: { id: number; name: string }) => (
<StupidHoverThing
key={outfit.id}
className="hover:bg-base/50 rounded-lg"
delayDuration={0}
text={outfit.name}
>
<button
className="group rounded-xl border border-surface0/50 bg-base/40 p-3 text-left transition hover:-translate-y-0.5 hover:border-surface1/80 hover:bg-surface0/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue/60"
onClick={async () => {
updateOutfit(outfit, acc);
await updateOutfit(outfit, acc);
setVisible(false);
}}
aria-label={`Wear ${outfit.name}`}
>
<LazyLoadedImage
imgId={`Outfit_${outfit.id}`}
alt={outfit.name}
className="w-32 h-32 rounded-md"
className="h-24 w-24 sm:h-28 sm:w-28 rounded-lg object-cover shadow-sm"
size="420x420"
lazyFetch={false}
/>
<p className="mt-3 text-xs font-medium text-text line-clamp-2">
{outfit.name}
</p>
</button>
</StupidHoverThing>
))}
</div>
) : (
<div className="rounded-xl border border-surface0/60 bg-base/50 p-6 text-sm text-subtext1">
No outfits found yet. Make one in the Roblox avatar editor,
then come back here.
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -16,7 +16,6 @@ import { useFriendsPresence } from "@/hooks/roblox/usePresence";
async function updateOutfit(outfit: { id: number }, acc: { id: number }) {
try {
// ocbwoy3 stupid idiot for using v3 api
const details = (await (
await proxyFetch(
`https://avatar.roblox.com/v1/outfits/${outfit.id}/details`
@@ -24,18 +23,7 @@ async function updateOutfit(outfit: { id: number }, acc: { id: number }) {
).json()) as {
id: number;
name: string;
bodyColors: Record<string, string>;
scale: Record<string, number>;
};
const detailsV3 = (await (
await proxyFetch(
`https://avatar.roblox.com/v3/outfits/${outfit.id}/details`
)
).json()) as {
id: number;
name: string;
assets: any[];
assets: Array<{ id: number; meta?: Record<string, unknown> }>;
bodyColors: Record<string, string>;
scale: Record<string, number>;
playerAvatarType: "R6" | "R15";
@@ -58,26 +46,44 @@ async function updateOutfit(outfit: { id: number }, acc: { id: number }) {
// u cant set avatar item scaling/rotation cuz roblox can't make good web apis
await proxyFetch(
`https://avatar.roblox.com/v1/avatar/set-wearing-assets`,
`https://avatar.roblox.com/v2/avatar/set-wearing-assets`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
assetIds: detailsV3.assets.map((a) => a.id).filter(Boolean)
assets: details.assets
.map((asset) => ({
id: asset.id,
meta: asset.meta
}))
.filter((asset) => Boolean(asset.id))
})
}
);
const avatarType =
details.playerAvatarType === "R15"
? 3
: details.playerAvatarType === "R6"
? 1
: null;
if (avatarType !== null) {
await proxyFetch(
`https://avatar.roblox.com/v1/avatar/set-player-avatar-type`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
playerAvatarType: detailsV3.playerAvatarType
playerAvatarType: avatarType
})
}
);
}
await proxyFetch(`https://avatar.roblox.com/v1/avatar/redraw-thumbnail`, {
method: "POST"
});
loadThumbnails([
{
@@ -112,15 +118,15 @@ export const QuickTopUI = React.memo(function () {
) : (
<></>
)}
<div className="z-50 fixed top-4 right-4 p-4 flex gap-2 items-center text-blue/75">
<div className="z-50 fixed top-4 right-4 flex gap-2 items-center text-text">
<StupidHoverThing text="Change Outfit">
<button
className="rounded-full bg-crust/50 flex items-center p-2"
className="rounded-full bg-surface0/70 ring-1 ring-surface1/60 flex items-center justify-center h-10 w-10 text-text shadow-sm transition hover:bg-surface1/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue/60"
onClick={() => {
setIsOutfitSelectorVisible((a) => !a);
}}
>
<ShirtIcon />
<ShirtIcon className="h-5 w-5" />
</button>
</StupidHoverThing>
@@ -131,15 +137,11 @@ export const QuickTopUI = React.memo(function () {
: `You have ${robux.toLocaleString()} Robux`
}
>
<div className="rounded-full bg-crust/50 flex items-center p-2">
<RobuxIcon className="w-6 h-6" />
{robux ? (
<p className="pl-1">
{robux ? robux.toLocaleString() : "???"}
<div className="rounded-full bg-surface0/70 ring-1 ring-surface1/60 flex items-center h-10 px-3 gap-2 text-text shadow-sm">
<RobuxIcon className="w-5 h-5 text-green" />
<p className="text-sm font-super-mono tabular-nums">
{robux ? robux.toLocaleString() : "..."}
</p>
) : (
<></>
)}
</div>
</StupidHoverThing>
</div>
@@ -149,12 +151,15 @@ export const QuickTopUI = React.memo(function () {
export const QuickTopUILogoPart = React.memo(function () {
return (
<div className="z-[15] relative top-4 left-4 p-4 flex gap-4 items-center text-blue">
<Link href="/" className="-m-1 w-8 h-8">
<img src="/icon-512.webp" className="w-8 h-8" alt="" />
<div className="z-15 relative top-4 left-4 flex gap-3 items-center rounded-full">
<Link
href="/"
className="flex h-8 w-8 items-center justify-center rounded-full"
>
<img src="/roblox.png" className="w-6 h-6" alt="" />
</Link>
<Link href="/" className="mt-2 gap-2 flex items-center">
<p>{"ocbwoy3-chan's roblox"}</p>
<Link href="/" className="gap-2 flex items-center text-sm font-medium">
<p>{"Roblox"}</p>
{/* <p className="text-surface2 line-clamp-1">
{process.env.NODE_ENV} {process.env.NEXT_PUBLIC_CWD}{" "}
{process.env.NEXT_PUBLIC_ARGV0}

View File

@@ -102,16 +102,19 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
type ChartTooltipContentProps =
RechartsPrimitive.TooltipContentProps<number | string, string> &
React.ComponentProps<"div"> & {
hideLabel?: boolean;
hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed";
nameKey?: string;
labelKey?: string;
}
};
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
ChartTooltipContentProps
>(
(
{
@@ -181,7 +184,7 @@ const ChartTooltipContent = React.forwardRef<
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className
)}
>
@@ -288,13 +291,18 @@ ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend;
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> &
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
type ChartLegendContentProps = React.ComponentProps<"div"> &
Pick<
RechartsPrimitive.DefaultLegendContentProps,
"payload" | "verticalAlign"
> & {
hideIcon?: boolean;
nameKey?: string;
}
};
const ChartLegendContent = React.forwardRef<
HTMLDivElement,
ChartLegendContentProps
>(
(
{

View File

@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
<ResizablePrimitive.PanelGroup
}: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
<ResizablePrimitive.Group
className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
"flex h-full w-full aria-[orientation=vertical]:flex-col",
className
)}
{...props}
@@ -24,12 +24,12 @@ const ResizableHandle = ({
withHandle,
className,
...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
}: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean;
}) => (
<ResizablePrimitive.PanelResizeHandle
<ResizablePrimitive.Separator
className={cn(
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
"relative flex items-center justify-center bg-border focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 aria-[orientation=vertical]:h-full aria-[orientation=vertical]:w-px aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:-translate-y-1/2 aria-[orientation=horizontal]:after:translate-x-0 [&[aria-orientation=horizontal]>div]:rotate-90",
className
)}
{...props}
@@ -39,7 +39,7 @@ const ResizableHandle = ({
<GripVertical className="h-2.5 w-2.5" />
</div>
)}
</ResizablePrimitive.PanelResizeHandle>
</ResizablePrimitive.Separator>
);
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -12,6 +12,7 @@ export function useCurrentAccount(): UserProfileDetails | null | false {
const query = useQuery<UserProfileDetails | false>({
queryKey: ["currentAccount"],
queryFn: async () => {
try {
const authed = await getLoggedInUser();
if (!authed) return false;
@@ -27,6 +28,9 @@ export function useCurrentAccount(): UserProfileDetails | null | false {
]).catch(() => {});
return user;
} catch {
return false;
}
},
staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false

View File

@@ -1,6 +1,5 @@
"use client";
import assert from "assert";
import { proxyFetch } from "./utils";
export type UserProfileDetails = {
@@ -28,7 +27,9 @@ export async function getLoggedInUser(): Promise<{
}
}
);
assert(data.ok);
if (!data.ok) {
return null;
}
const J = await data.json();
if (J.errors) {
return null;

View File

@@ -3,76 +3,76 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
"dev": "bunx --bun next dev --turbopack",
"build": "bunx --bun next build",
"start": "bunx --bun next start",
"lint": "bunx --bun next lint"
},
"dependencies": {
"@catppuccin/tailwindcss": "^0.1.6",
"@hookform/resolvers": "^5.1.1",
"@ocbwoy3/libocbwoy3": "^0.0.5",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.1.14",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.15",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
"@radix-ui/react-hover-card": "^1.1.14",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.15",
"@radix-ui/react-navigation-menu": "^1.2.13",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.7",
"@radix-ui/react-scroll-area": "^1.2.9",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.9",
"@radix-ui/react-toggle-group": "^1.1.10",
"@radix-ui/react-tooltip": "^1.2.7",
"@tailwindcss/line-clamp": "^0.4.4",
"@tanstack/query-async-storage-persister": "^5.85.3",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-query-devtools": "^5.85.3",
"@tanstack/react-query-persist-client": "^5.85.3",
"@types/bun": "^1.2.19",
"@catppuccin/tailwindcss": "^1.0.0",
"@hookform/resolvers": "^5.2.2",
"@ocbwoy3/libocbwoy3": "^0.0.6",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/query-async-storage-persister": "^5.90.14",
"@tanstack/react-query": "^5.90.12",
"@tanstack/react-query-devtools": "^5.91.1",
"@tanstack/react-query-persist-client": "^5.90.14",
"@types/bun": "^1.3.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.525.0",
"next": "15.1.6",
"lucide-react": "^0.562.0",
"next": "^16.1.1",
"next-themes": "^0.4.6",
"noblox.js": "^6.2.0",
"react": "^19.0.0",
"react-day-picker": "^9.8.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.60.0",
"react-resizable-panels": "^3.0.3",
"recharts": "2.15.4",
"sonner": "^2.0.6",
"tailwind-merge": "^3.0.1",
"react": "^19.2.3",
"react-day-picker": "^9.13.0",
"react-dom": "^19.2.3",
"react-hook-form": "^7.69.0",
"react-resizable-panels": "^4.0.15",
"recharts": "3.6.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.0.5"
"zod": "^4.2.1"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"postcss": "^8",
"tailwindcss": "^3.4.1"
"typescript": "^5.9.3",
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@tailwindcss/postcss": "^4.1.18",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18"
}
}

View File

@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {}
"@tailwindcss/postcss": {}
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
public/favicon2.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
public/roblox.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,7 +1,7 @@
import type { Config } from "tailwindcss";
export default {
darkMode: ["class"],
darkMode: "class",
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
@@ -12,6 +12,19 @@ export default {
colors: {
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
base: "hsl(var(--ctp-base))",
mantle: "hsl(var(--ctp-mantle))",
crust: "hsl(var(--ctp-crust))",
text: "hsl(var(--ctp-text))",
subtext0: "hsl(var(--ctp-subtext0))",
subtext1: "hsl(var(--ctp-subtext1))",
surface0: "hsl(var(--ctp-surface0))",
surface1: "hsl(var(--ctp-surface1))",
surface2: "hsl(var(--ctp-surface2))",
blue: "hsl(var(--ctp-blue))",
green: "hsl(var(--ctp-green))",
yellow: "hsl(var(--ctp-yellow))",
red: "hsl(var(--ctp-red))",
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))"
@@ -60,10 +73,7 @@ export default {
"accent-foreground":
"hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
"primary-foreground":
"hsl(var(--sidebar-primary-foreground))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))"
ring: "hsl(var(--sidebar-ring))"
}
},
borderRadius: {
@@ -72,22 +82,6 @@ export default {
sm: "calc(var(--radius) - 4px)"
},
keyframes: {
"accordion-down": {
from: {
height: "0"
},
to: {
height: "var(--radix-accordion-content-height)"
}
},
"accordion-up": {
from: {
height: "var(--radix-accordion-content-height)"
},
to: {
height: "0"
}
},
"accordion-down": {
from: {
height: "0"
@@ -106,19 +100,12 @@ export default {
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out"
}
}
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/line-clamp"),
require("@catppuccin/tailwindcss")({
prefix: false,
defaultFlavour: "mocha"
})
require("tailwindcss-animate")
]
} satisfies Config;

View File

@@ -1,7 +1,11 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
@@ -11,7 +15,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
@@ -19,9 +23,19 @@
}
],
"paths": {
"@/*": ["./*"]
"@/*": [
"./*"
]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}