fixxxxxxxx
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,21 +38,24 @@ export default function RootLayout({
|
||||
>
|
||||
<ReactQueryProvider>
|
||||
<TooltipProvider>
|
||||
<main>
|
||||
{/* <Image
|
||||
src={"/bg.png"}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 opacity-25"
|
||||
alt=""
|
||||
/> */}
|
||||
<QuickTopUI />
|
||||
<div className="backdrop-blur-lg z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||
<QuickTopUILogoPart />
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<Toaster />
|
||||
<GameLaunchProvider>
|
||||
<main>
|
||||
{/* <Image
|
||||
src={"/bg.png"}
|
||||
width={1920}
|
||||
height={1080}
|
||||
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 opacity-25"
|
||||
alt=""
|
||||
/> */}
|
||||
<QuickTopUI />
|
||||
<div className="backdrop-blur-lg z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
|
||||
<QuickTopUILogoPart />
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<GameLaunchDialog />
|
||||
<Toaster />
|
||||
</GameLaunchProvider>
|
||||
</TooltipProvider>
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
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 };
|
||||
|
||||
@@ -12,21 +12,25 @@ export function useCurrentAccount(): UserProfileDetails | null | false {
|
||||
const query = useQuery<UserProfileDetails | false>({
|
||||
queryKey: ["currentAccount"],
|
||||
queryFn: async () => {
|
||||
const authed = await getLoggedInUser();
|
||||
if (!authed) return false;
|
||||
try {
|
||||
const authed = await getLoggedInUser();
|
||||
if (!authed) return false;
|
||||
|
||||
const user = await getUserByUserId(authed.id.toString());
|
||||
const user = await getUserByUserId(authed.id.toString());
|
||||
|
||||
loadThumbnails([
|
||||
{
|
||||
type: "AvatarHeadShot",
|
||||
targetId: authed.id,
|
||||
format: "webp",
|
||||
size: "720x720"
|
||||
}
|
||||
]).catch(() => {});
|
||||
loadThumbnails([
|
||||
{
|
||||
type: "AvatarHeadShot",
|
||||
targetId: authed.id,
|
||||
format: "webp",
|
||||
size: "720x720"
|
||||
}
|
||||
]).catch(() => {});
|
||||
|
||||
return user;
|
||||
return user;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
staleTime: 1000 * 60 * 5,
|
||||
refetchOnWindowFocus: false
|
||||
|
||||
@@ -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;
|
||||
|
||||
114
package.json
114
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
BIN
public/favicon2.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
BIN
public/roblox.png
Normal file
BIN
public/roblox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -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;
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user