diff --git a/bun.lock b/bun.lock index 33b0a85..3a4702e 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "drizzle-orm": "^0.41.0", "embla-carousel-react": "^8.5.2", "input-otp": "^1.4.2", + "ioredis": "^5.6.0", "ky": "^1.8.0", "lucide-react": "^0.487.0", "next": "^15.2.3", @@ -218,6 +219,8 @@ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + "@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="], + "@next/env": ["@next/env@15.2.4", "", {}, "sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw=="], @@ -434,6 +437,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -480,6 +485,8 @@ "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + "detect-libc": ["detect-libc@2.0.3", "", {}, "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], @@ -522,6 +529,8 @@ "intl-messageformat": ["intl-messageformat@10.7.16", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.4", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.2", "tslib": "^2.8.0" } }, "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug=="], + "ioredis": ["ioredis@5.6.0", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg=="], + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "is-what": ["is-what@4.1.16", "", {}, "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A=="], @@ -560,6 +569,10 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], + + "lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lucide-react": ["lucide-react@0.487.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aKqhOQ+YmFnwq8dWgGjOuLc8V1R9/c/yOd+zDY4+ohsR2Jo05lSGc3WsstYPIzcTpeosN7LoCkLReUUITvaIvw=="], @@ -622,6 +635,10 @@ "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], + + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], "remeda": ["remeda@2.21.2", "", { "dependencies": { "type-fest": "^4.37.0" } }, "sha512-wdhkMDou8HRpD7RnxKJ/FHJWEGXRH7jV/pb0NsdLLSoBo+G9RjtxcY41hVhogLfEMkThk6aySKjs+Yd6PnpzBA=="], @@ -648,6 +665,8 @@ "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], diff --git a/package.json b/package.json index 8f0b933..86ef112 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "drizzle-orm": "^0.41.0", "embla-carousel-react": "^8.5.2", "input-otp": "^1.4.2", + "ioredis": "^5.6.0", "ky": "^1.8.0", "lucide-react": "^0.487.0", "next": "^15.2.3", diff --git a/scripts/refresh-leaderboards.ts b/scripts/refresh-leaderboards.ts new file mode 100644 index 0000000..7d60dad --- /dev/null +++ b/scripts/refresh-leaderboards.ts @@ -0,0 +1,27 @@ +import { LeaderboardService } from '@/server/services/leaderboard' +import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' + +const CHANNEL_IDS = [RANKED_CHANNEL, VANILLA_CHANNEL] + +async function refresh() { + const service = new LeaderboardService() + + for (const channelId of CHANNEL_IDS) { + try { + console.log(`refreshing leaderboard for ${channelId}...`) + await service.refreshLeaderboard(channelId) + } catch (err) { + console.error(`failed to refresh ${channelId}:`, err) + } + } +} + +// run if called directly +if (require.main === module) { + refresh() + .then(() => process.exit(0)) + .catch((err) => { + console.error('refresh failed:', err) + process.exit(1) + }) +} diff --git a/src/app/players/[id]/page.tsx b/src/app/players/[id]/page.tsx index c21436c..ec4f201 100644 --- a/src/app/players/[id]/page.tsx +++ b/src/app/players/[id]/page.tsx @@ -1,5 +1,6 @@ import { UserInfo } from '@/app/players/[id]/user' import { auth } from '@/server/auth' +import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { HydrateClient, api } from '@/trpc/server' import { Suspense } from 'react' @@ -11,12 +12,28 @@ export default async function PlayerPage({ const session = await auth() const { id } = await params if (id) { - await api.history.user_games.prefetch({ - user_id: id, - }) - await api.discord.get_user_by_id.prefetch({ - user_id: id, - }) + await Promise.all([ + api.history.user_games.prefetch({ + user_id: id, + }), + api.discord.get_user_by_id.prefetch({ + user_id: id, + }), + api.leaderboard.get_leaderboard.prefetch({ + channel_id: RANKED_CHANNEL, + }), + api.leaderboard.get_leaderboard.prefetch({ + channel_id: VANILLA_CHANNEL, + }), + api.leaderboard.get_user_rank.prefetch({ + channel_id: RANKED_CHANNEL, + user_id: id, + }), + api.leaderboard.get_user_rank.prefetch({ + channel_id: VANILLA_CHANNEL, + user_id: id, + }), + ]) } return ( diff --git a/src/app/players/[id]/user.tsx b/src/app/players/[id]/user.tsx index 9aae6e8..f1635c1 100644 --- a/src/app/players/[id]/user.tsx +++ b/src/app/players/[id]/user.tsx @@ -23,7 +23,9 @@ import { } from '@/components/ui/table' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { cn } from '@/lib/utils' +import { RANKED_CHANNEL, VANILLA_CHANNEL } from '@/shared/constants' import { api } from '@/trpc/react' +import { isNotNull } from 'drizzle-orm' import { ArrowDownCircle, ArrowUpCircle, @@ -33,12 +35,13 @@ import { ChevronUp, Clock, Filter, - Medal, MinusCircle, - Users, + Star, + Trophy, } from 'lucide-react' import { useFormatter } from 'next-intl' import { useParams } from 'next/navigation' +import { isNonNullish } from 'remeda' const numberFormatter = new Intl.NumberFormat('en-US', { signDisplay: 'exceptZero', @@ -46,17 +49,59 @@ const numberFormatter = new Intl.NumberFormat('en-US', { const dateFormatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'long', }) + export function UserInfo() { const format = useFormatter() const [filter, setFilter] = useState('all') + const [leaderboardFilter, setLeaderboardFilter] = useState('all') const { id } = useParams() if (!id || typeof id !== 'string') return null - const [games] = api.history.user_games.useSuspenseQuery({ user_id: id }) + // Fetch games data unconditionally + const gamesQuery = api.history.user_games.useSuspenseQuery({ user_id: id }) + const games = gamesQuery[0] || [] // Ensure games is always an array + const [discord_user] = api.discord.get_user_by_id.useSuspenseQuery({ user_id: id, }) + + // Mock data for the two leaderboards - replace with actual API calls + const [rankedLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery({ + channel_id: RANKED_CHANNEL, + }) + const [vanillaLeaderboard] = api.leaderboard.get_leaderboard.useSuspenseQuery( + { + channel_id: VANILLA_CHANNEL, + } + ) + const [vanillaUserRank] = api.leaderboard.get_user_rank.useSuspenseQuery({ + channel_id: VANILLA_CHANNEL, + user_id: id, + }) + const [rankedUserRank] = api.leaderboard.get_user_rank.useSuspenseQuery({ + channel_id: RANKED_CHANNEL, + user_id: id, + }) + + console.log(rankedLeaderboard, vanillaLeaderboard) + + // Filter games by leaderboard if needed + const filteredGamesByLeaderboard = + leaderboardFilter === 'all' + ? games + : games.filter((game) => game.gameType === leaderboardFilter) + + // Filter by result + const filteredGames = + filter === 'all' + ? filteredGamesByLeaderboard + : filter === 'wins' + ? filteredGamesByLeaderboard.filter((game) => game.result === 'win') + : filter === 'losses' + ? filteredGamesByLeaderboard.filter((game) => game.result === 'loss') + : filteredGamesByLeaderboard.filter((game) => game.result === 'tie') + const games_played = games.length let wins = 0 let losses = 0 @@ -70,6 +115,7 @@ export function UserInfo() { ties++ } } + const profileData = { username: discord_user.username, avatar: discord_user.avatar_url, @@ -77,13 +123,20 @@ export function UserInfo() { wins, losses, ties, - winRate: Math.round((wins / games_played) * 100), - rank: 1, + winRate: games_played > 0 ? Math.round((wins / games_played) * 100) : 0, } const lastGame = games.at(0) const firstGame = games.at(-1) - console.log(lastGame) + + // Get last games for each leaderboard + const lastGameLeaderboard1 = games + .filter((game) => game.gameType === 'ranked') + .at(0) + const lastGameLeaderboard2 = games + .filter((game) => game.gameType.toLowerCase() === 'vanilla') + .at(0) + return (
@@ -113,52 +166,101 @@ export function UserInfo() { <>No games played yet )}

-
- - - Rank #342 - - - - Gold - +
+ {!!rankedLeaderboard && ( + + + Ranked Queue:{' '} + {isNonNullish(rankedUserRank?.rank) + ? `#${rankedUserRank.rank}` + : 'N/A'} + + )} + {!!vanillaLeaderboard && ( + + + Vanilla Queue:{' '} + {isNonNullish(vanillaLeaderboard?.rank) + ? `#${vanillaLeaderboard.rank}` + : 'N/A'} + + )}
-
-
Current MMR
-
- {Math.trunc( - lastGame ? lastGame.playerMmr + lastGame.mmrChange : 200 - )} -
-
- {!!lastGame && - (lastGame.mmrChange > 0 ? ( - - - {numberFormatter.format( - Math.trunc(lastGame.mmrChange) - )}{' '} - last match - - ) : ( - - - {numberFormatter.format( - Math.trunc(lastGame.mmrChange) - )}{' '} - last match - - ))} -
+
+ {lastGameLeaderboard1 && ( +
+
+ Ranked Queue MMR +
+
+ {Math.trunc( + lastGameLeaderboard1.playerMmr + + lastGameLeaderboard1.mmrChange + )} +
+
+ {lastGameLeaderboard1.mmrChange > 0 ? ( + + + {numberFormatter.format( + Math.trunc(lastGameLeaderboard1.mmrChange) + )}{' '} + last match + + ) : ( + + + {numberFormatter.format( + Math.trunc(lastGameLeaderboard1.mmrChange) + )}{' '} + last match + + )} +
+
+ )} + + {lastGameLeaderboard2 && ( +
+
+ Leaderboard 2 MMR +
+
+ {Math.trunc( + lastGameLeaderboard2.playerMmr + + lastGameLeaderboard2.mmrChange + )} +
+
+ {lastGameLeaderboard2.mmrChange > 0 ? ( + + + {numberFormatter.format( + Math.trunc(lastGameLeaderboard2.mmrChange) + )}{' '} + last match + + ) : ( + + + {numberFormatter.format( + Math.trunc(lastGameLeaderboard2.mmrChange) + )}{' '} + last match + + )} +
+
+ )}
@@ -183,14 +285,14 @@ export function UserInfo() { title='Losses' value={profileData.losses} icon={} - description={`${Math.round((profileData.losses / profileData.games) * 100)}% loss rate`} + description={`${profileData.games > 0 ? Math.round((profileData.losses / profileData.games) * 100) : 0}% loss rate`} accentColor='text-rose-500' /> } - description={`${Math.round((profileData.ties / profileData.games) * 100)}% tie rate`} + description={`${profileData.games > 0 ? Math.round((profileData.ties / profileData.games) * 100) : 0}% tie rate`} accentColor='text-amber-500' />
@@ -204,6 +306,23 @@ export function UserInfo() {
+
+ + +
+ ) } -function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) { +function SidebarHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) { +function SidebarFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) @@ -360,21 +360,21 @@ function SidebarSeparator({ }: React.ComponentProps) { return ( ) } -function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { +function SidebarContent({ className, ...props }: React.ComponentProps<'div'>) { return (
) { ) } -function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) { +function SidebarGroup({ className, ...props }: React.ComponentProps<'div'>) { return (
) @@ -397,16 +397,16 @@ function SidebarGroupLabel({ className, asChild = false, ...props -}: React.ComponentProps<"div"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "div" +}: React.ComponentProps<'div'> & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'div' return ( svg]:size-4 [&>svg]:shrink-0", - "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0", + 'flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-hidden ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', + 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', className )} {...props} @@ -418,18 +418,18 @@ function SidebarGroupAction({ className, asChild = false, ...props -}: React.ComponentProps<"button"> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button" +}: React.ComponentProps<'button'> & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'button' return ( svg]:size-4 [&>svg]:shrink-0", + 'absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', // Increases the hit area of the button on mobile. - "after:absolute after:-inset-2 md:after:hidden", - "group-data-[collapsible=icon]:hidden", + 'after:-inset-2 after:absolute md:after:hidden', + 'group-data-[collapsible=icon]:hidden', className )} {...props} @@ -440,57 +440,57 @@ function SidebarGroupAction({ function SidebarGroupContent({ className, ...props -}: React.ComponentProps<"div">) { +}: React.ComponentProps<'div'>) { return (
) } -function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) { +function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) { return (
    ) } -function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) { +function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { return (
  • ) } const sidebarMenuButtonVariants = cva( - "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0", + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', { variants: { variant: { - default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground", + default: 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground', outline: - "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]", + 'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]', }, size: { - default: "h-8 text-sm", - sm: "h-7 text-xs", - lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!", + default: 'h-8 text-sm', + sm: 'h-7 text-xs', + lg: 'h-12 text-sm group-data-[collapsible=icon]:p-0!', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } ) @@ -498,23 +498,23 @@ const sidebarMenuButtonVariants = cva( function SidebarMenuButton({ asChild = false, isActive = false, - variant = "default", - size = "default", + variant = 'default', + size = 'default', tooltip, className, ...props -}: React.ComponentProps<"button"> & { +}: React.ComponentProps<'button'> & { asChild?: boolean isActive?: boolean tooltip?: string | React.ComponentProps } & VariantProps) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' const { isMobile, state } = useSidebar() const button = ( {button}