fixxxxxxxx
This commit is contained in:
79
components/providers/GameLaunchDialog.tsx
Normal file
79
components/providers/GameLaunchDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
components/providers/GameLaunchProvider.tsx
Normal file
60
components/providers/GameLaunchProvider.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
retry: true
|
||||
}
|
||||
}
|
||||
});
|
||||
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}>
|
||||
|
||||
35
components/providers/game-launch-store.ts
Normal file
35
components/providers/game-launch-store.ts
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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!"
|
||||
)}
|
||||
|
||||
@@ -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,37 +34,99 @@ 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
|
||||
key={outfit.id}
|
||||
className="hover:bg-base/50 rounded-lg"
|
||||
onClick={async () => {
|
||||
updateOutfit(outfit, acc);
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<LazyLoadedImage
|
||||
imgId={`Outfit_${outfit.id}`}
|
||||
alt={outfit.name}
|
||||
className="w-32 h-32 rounded-md"
|
||||
/>
|
||||
</button>
|
||||
</StupidHoverThing>
|
||||
))}
|
||||
<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}
|
||||
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 () => {
|
||||
await updateOutfit(outfit, acc);
|
||||
setVisible(false);
|
||||
}}
|
||||
aria-label={`Wear ${outfit.name}`}
|
||||
>
|
||||
<LazyLoadedImage
|
||||
imgId={`Outfit_${outfit.id}`}
|
||||
alt={outfit.name}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
);
|
||||
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: 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() : "???"}
|
||||
</p>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
type ChartLegendContentProps = React.ComponentProps<"div"> &
|
||||
Pick<
|
||||
RechartsPrimitive.DefaultLegendContentProps,
|
||||
"payload" | "verticalAlign"
|
||||
> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
};
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
ChartLegendContentProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user