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

View File

@@ -1,6 +1,5 @@
@tailwind base; @config "../tailwind.config.ts";
@tailwind components; @import "tailwindcss";
@tailwind utilities;
body { body {
font-family: SF Pro Display, Geist; font-family: SF Pro Display, Geist;
@@ -12,6 +11,20 @@ body {
@layer base { @layer base {
:root { :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 */ --background: 240 21.052631735801697% 14.901961386203766%; /* base */
--foreground: 226 63.93442749977112% 88.03921341896057%; /* text */ --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 { @layer base {
* { * {
@apply border-border; @apply border-border;
@@ -88,3 +139,6 @@ body {
scrollbar-width: 0; 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 Image from "next/image";
import { QuickTopUI, QuickTopUILogoPart } from "@/components/site/QuickTopUI"; import { QuickTopUI, QuickTopUILogoPart } from "@/components/site/QuickTopUI";
import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider"; import { ReactQueryProvider } from "@/components/providers/ReactQueryProvider";
import { GameLaunchProvider } from "@/components/providers/GameLaunchProvider";
import { GameLaunchDialog } from "@/components/providers/GameLaunchDialog";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -18,8 +20,10 @@ const geistMono = Geist_Mono({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "home | ocbwoy3-chan's roblox", title: "Home | Roblox",
description: "roblox meets next.js i think" 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({ export default function RootLayout({
@@ -34,21 +38,24 @@ export default function RootLayout({
> >
<ReactQueryProvider> <ReactQueryProvider>
<TooltipProvider> <TooltipProvider>
<main> <GameLaunchProvider>
{/* <Image <main>
src={"/bg.png"} {/* <Image
width={1920} src={"/bg.png"}
height={1080} width={1920}
className="w-screen h-screen bg-blend-hard-light fixed top-0 left-0 opacity-25" height={1080}
alt="" 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"> <QuickTopUI />
<QuickTopUILogoPart /> <div className="backdrop-blur-lg z-10 isolate overflow-scroll no-scrollbar w-screen max-h-screen h-screen antialiased overflow-x-hidden">
{children} <QuickTopUILogoPart />
</div> {children}
</main> </div>
<Toaster /> </main>
<GameLaunchDialog />
<Toaster />
</GameLaunchProvider>
</TooltipProvider> </TooltipProvider>
</ReactQueryProvider> </ReactQueryProvider>
</body> </body>

View File

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

View File

@@ -42,7 +42,7 @@ export default function UserProfileContent({
// Set dynamic document title // Set dynamic document title
useEffect(() => { useEffect(() => {
if (profile?.displayName) { if (profile?.displayName) {
document.title = `${profile.displayName}'s profile | ocbwoy3-chan's roblox`; document.title = `${profile.displayName}'s profile | Roblox`;
} }
}, [profile]); }, [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"; "use client";
import { ReactNode, useEffect } from "react"; import { ReactNode, useEffect, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { persistQueryClient } from "@tanstack/react-query-persist-client";
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"; import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
@@ -11,19 +11,22 @@ interface Props {
} }
export function ReactQueryProvider({ children }: Props) { export function ReactQueryProvider({ children }: Props) {
const queryClient = new QueryClient({ const [queryClient] = useState(
defaultOptions: { () =>
queries: { new QueryClient({
staleTime: 1000 * 60 * 5, // 5 minutes defaultOptions: {
retry: true queries: {
} staleTime: 1000 * 60 * 5, // 5 minutes
} retry: true
}); }
}
})
);
// will cause bun to SEGFAULT // will cause bun to SEGFAULT
useEffect(() => { useEffect(() => {
if (!window) return; if (typeof window === "undefined") return;
// Persist to localStorage (safe, runs client-side) // Persist to localStorage (safe, runs client-side)
const localStoragePersister = createAsyncStoragePersister({ const localStoragePersister = createAsyncStoragePersister({
storage: window.localStorage storage: window.localStorage
@@ -34,7 +37,7 @@ export function ReactQueryProvider({ children }: Props) {
persister: localStoragePersister, persister: localStoragePersister,
maxAge: 1000 * 60 * 60 // 1 hour max maxAge: 1000 * 60 * 60 // 1 hour max
}); });
}, [window || "wtf"]); }, [queryClient]);
return ( return (
<QueryClientProvider client={queryClient}> <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 { ContextMenuItem } from "@radix-ui/react-context-menu";
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import { useGameLaunch } from "@/components/providers/GameLaunchProvider";
interface GameCardProps { interface GameCardProps {
game: ContentMetadata; game: ContentMetadata;
} }
export const GameCard = React.memo(function GameCard({ game }: GameCardProps) { export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
const { launchGame } = useGameLaunch();
return ( return (
<ContextMenu> <ContextMenu>
<ContextMenuTrigger> <ContextMenuTrigger>
@@ -78,7 +81,7 @@ export const GameCard = React.memo(function GameCard({ game }: GameCardProps) {
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem <ContextMenuItem
onClick={() => { onClick={() => {
window.location.href = `roblox://placeId=${game.rootPlaceId}`; launchGame(game.rootPlaceId.toString());
}} }}
> >
Play Play

View File

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

View File

@@ -2,12 +2,12 @@
import { useAvatarOutfits } from "@/hooks/roblox/useAvatarOutfits"; import { useAvatarOutfits } from "@/hooks/roblox/useAvatarOutfits";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn, proxyFetch } from "@/lib/utils";
import LazyLoadedImage from "../util/LazyLoadedImage"; import LazyLoadedImage from "../util/LazyLoadedImage";
import { StupidHoverThing } from "../util/MiscStuff"; import { StupidHoverThing } from "../util/MiscStuff";
import { loadThumbnails } from "@/lib/thumbnailLoader"; import { loadThumbnails } from "@/lib/thumbnailLoader";
import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount"; import { useCurrentAccount } from "@/hooks/roblox/useCurrentAccount";
import { useEffect } from "react"; import { useEffect } from "react";
import { X } from "lucide-react";
type OutfitSelectorProps = { type OutfitSelectorProps = {
setVisible: (visible: boolean) => void; setVisible: (visible: boolean) => void;
@@ -25,7 +25,7 @@ export function OutfitSelector({
const acc = useCurrentAccount(); const acc = useCurrentAccount();
useEffect(() => { useEffect(() => {
if (!outfits) return; if (!outfits || outfits.length === 0) return;
loadThumbnails( loadThumbnails(
outfits.map((a) => ({ outfits.map((a) => ({
type: "Outfit", type: "Outfit",
@@ -34,37 +34,99 @@ export function OutfitSelector({
size: "420x420" size: "420x420"
})) }))
).catch(() => {}); ).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 ( return (
<div className="z-30 isolate absolute inset-0 flex items-center justify-center bg-crust/50"> <div
<button className="fixed inset-0 z-40 flex items-center justify-center bg-mantle/70 backdrop-blur-sm"
className="z-10 absolute w-screen h-screen cursor-default" onClick={() => setVisible(false)}
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="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 }) => ( <div className="flex items-center justify-between border-b border-surface0/60 px-6 py-4">
<StupidHoverThing key={outfit.id} delayDuration={0} text={outfit.name}> <div>
<button <p className="text-lg font-semibold text-text">Outfits</p>
key={outfit.id} <p className="text-xs text-subtext1">
className="hover:bg-base/50 rounded-lg" Pick a look to update your avatar instantly.
onClick={async () => { </p>
updateOutfit(outfit, acc); </div>
setVisible(false); <Button
}} variant="ghost"
> size="icon"
<LazyLoadedImage onClick={() => setVisible(false)}
imgId={`Outfit_${outfit.id}`} aria-label="Close outfit chooser"
alt={outfit.name} >
className="w-32 h-32 rounded-md" <X className="h-4 w-4" />
/> </Button>
</button> </div>
</StupidHoverThing>
))} <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>
</div> </div>
); );

View File

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

View File

@@ -102,16 +102,19 @@ ${colorConfig
const ChartTooltip = RechartsPrimitive.Tooltip; const ChartTooltip = RechartsPrimitive.Tooltip;
const ChartTooltipContent = React.forwardRef< type ChartTooltipContentProps =
HTMLDivElement, RechartsPrimitive.TooltipContentProps<number | string, string> &
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div"> & { React.ComponentProps<"div"> & {
hideLabel?: boolean; hideLabel?: boolean;
hideIndicator?: boolean; hideIndicator?: boolean;
indicator?: "line" | "dot" | "dashed"; indicator?: "line" | "dot" | "dashed";
nameKey?: string; nameKey?: string;
labelKey?: string; labelKey?: string;
} };
const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
ChartTooltipContentProps
>( >(
( (
{ {
@@ -181,7 +184,7 @@ const ChartTooltipContent = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( 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 className
)} )}
> >
@@ -288,13 +291,18 @@ ChartTooltipContent.displayName = "ChartTooltip";
const ChartLegend = RechartsPrimitive.Legend; const ChartLegend = RechartsPrimitive.Legend;
type ChartLegendContentProps = React.ComponentProps<"div"> &
Pick<
RechartsPrimitive.DefaultLegendContentProps,
"payload" | "verticalAlign"
> & {
hideIcon?: boolean;
nameKey?: string;
};
const ChartLegendContent = React.forwardRef< const ChartLegendContent = React.forwardRef<
HTMLDivElement, HTMLDivElement,
React.ComponentProps<"div"> & ChartLegendContentProps
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
hideIcon?: boolean;
nameKey?: string;
}
>( >(
( (
{ {

View File

@@ -8,10 +8,10 @@ import { cn } from "@/lib/utils";
const ResizablePanelGroup = ({ const ResizablePanelGroup = ({
className, className,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => ( }: React.ComponentProps<typeof ResizablePrimitive.Group>) => (
<ResizablePrimitive.PanelGroup <ResizablePrimitive.Group
className={cn( 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 className
)} )}
{...props} {...props}
@@ -24,12 +24,12 @@ const ResizableHandle = ({
withHandle, withHandle,
className, className,
...props ...props
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & { }: React.ComponentProps<typeof ResizablePrimitive.Separator> & {
withHandle?: boolean; withHandle?: boolean;
}) => ( }) => (
<ResizablePrimitive.PanelResizeHandle <ResizablePrimitive.Separator
className={cn( 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 className
)} )}
{...props} {...props}
@@ -39,7 +39,7 @@ const ResizableHandle = ({
<GripVertical className="h-2.5 w-2.5" /> <GripVertical className="h-2.5 w-2.5" />
</div> </div>
)} )}
</ResizablePrimitive.PanelResizeHandle> </ResizablePrimitive.Separator>
); );
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; export { ResizablePanelGroup, ResizablePanel, ResizableHandle };

View File

@@ -12,21 +12,25 @@ export function useCurrentAccount(): UserProfileDetails | null | false {
const query = useQuery<UserProfileDetails | false>({ const query = useQuery<UserProfileDetails | false>({
queryKey: ["currentAccount"], queryKey: ["currentAccount"],
queryFn: async () => { queryFn: async () => {
const authed = await getLoggedInUser(); try {
if (!authed) return false; const authed = await getLoggedInUser();
if (!authed) return false;
const user = await getUserByUserId(authed.id.toString()); const user = await getUserByUserId(authed.id.toString());
loadThumbnails([ loadThumbnails([
{ {
type: "AvatarHeadShot", type: "AvatarHeadShot",
targetId: authed.id, targetId: authed.id,
format: "webp", format: "webp",
size: "720x720" size: "720x720"
} }
]).catch(() => {}); ]).catch(() => {});
return user; return user;
} catch {
return false;
}
}, },
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
refetchOnWindowFocus: false refetchOnWindowFocus: false

View File

@@ -1,6 +1,5 @@
"use client"; "use client";
import assert from "assert";
import { proxyFetch } from "./utils"; import { proxyFetch } from "./utils";
export type UserProfileDetails = { 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(); const J = await data.json();
if (J.errors) { if (J.errors) {
return null; return null;

View File

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

View File

@@ -1,7 +1,7 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: { 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"; import type { Config } from "tailwindcss";
export default { export default {
darkMode: ["class"], darkMode: "class",
content: [ content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
@@ -12,6 +12,19 @@ export default {
colors: { colors: {
background: "hsl(var(--background))", background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))", 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: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))" foreground: "hsl(var(--card-foreground))"
@@ -60,10 +73,7 @@ export default {
"accent-foreground": "accent-foreground":
"hsl(var(--sidebar-accent-foreground))", "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))", border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))", ring: "hsl(var(--sidebar-ring))"
"primary-foreground":
"hsl(var(--sidebar-primary-foreground))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))"
} }
}, },
borderRadius: { borderRadius: {
@@ -72,22 +82,6 @@ export default {
sm: "calc(var(--radius) - 4px)" sm: "calc(var(--radius) - 4px)"
}, },
keyframes: { 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": { "accordion-down": {
from: { from: {
height: "0" height: "0"
@@ -106,19 +100,12 @@ export default {
} }
}, },
animation: { 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-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out" "accordion-up": "accordion-up 0.2s ease-out"
} }
} }
}, },
plugins: [ plugins: [
require("tailwindcss-animate"), require("tailwindcss-animate")
require("@tailwindcss/line-clamp"),
require("@catppuccin/tailwindcss")({
prefix: false,
defaultFlavour: "mocha"
})
] ]
} satisfies Config; } satisfies Config;

View File

@@ -1,27 +1,41 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2017", "target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"], "lib": [
"allowJs": true, "dom",
"skipLibCheck": true, "dom.iterable",
"strict": true, "esnext"
"noEmit": true, ],
"esModuleInterop": true, "allowJs": true,
"module": "esnext", "skipLibCheck": true,
"moduleResolution": "bundler", "strict": true,
"resolveJsonModule": true, "noEmit": true,
"isolatedModules": true, "esModuleInterop": true,
"jsx": "preserve", "module": "esnext",
"incremental": true, "moduleResolution": "bundler",
"plugins": [ "resolveJsonModule": true,
{ "isolatedModules": true,
"name": "next" "jsx": "react-jsx",
} "incremental": true,
], "plugins": [
"paths": { {
"@/*": ["./*"] "name": "next"
} }
}, ],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "paths": {
"exclude": ["node_modules"] "@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }