added basic profile

This commit is contained in:
MasterGordon 2025-06-14 03:21:36 +02:00
parent d591553872
commit 781d80f5e5
7 changed files with 194 additions and 37 deletions

View File

@ -20,7 +20,7 @@ import { getWeight, lootboxes } from "../../shared/lootboxes";
import { weightedPickRandom } from "../../shared/utils"; import { weightedPickRandom } from "../../shared/utils";
import { emit } from "../events"; import { emit } from "../events";
import { Game } from "../schema"; import { Game } from "../schema";
import { and, eq, gt } from "drizzle-orm"; import { and, count, eq, gt, max, not, sum } from "drizzle-orm";
import dayjs from "dayjs"; import dayjs from "dayjs";
const secret = process.env.SECRET!; const secret = process.env.SECRET!;
@ -185,11 +185,7 @@ export const userController = createController({
}), }),
async ({ id }, { db }) => { async ({ id }, { db }) => {
const now = dayjs(); const now = dayjs();
const firstOfYear = now const firstOfYear = now.startOf("year");
.set("day", 0)
.set("month", 0)
.set("hour", 0)
.set("minute", 0);
const gamesOfUser = await db.query.Game.findMany({ const gamesOfUser = await db.query.Game.findMany({
where: and(eq(Game.user, id), gt(Game.finished, firstOfYear.valueOf())), where: and(eq(Game.user, id), gt(Game.finished, firstOfYear.valueOf())),
}); });
@ -203,4 +199,34 @@ export const userController = createController({
return heat; return heat;
}, },
), ),
getProfile: createEndpoint(
z.object({
id: z.string(),
}),
async ({ id }, { db }) => {
const [{ value: totalGames }] = await db
.select({
value: count(),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
const [{ value: highestStage }] = await db
.select({
value: max(Game.stage),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
const [{ value: totalStages }] = await db
.select({
value: sum(Game.stage),
})
.from(Game)
.where(and(eq(Game.user, id), not(eq(Game.finished, 0))));
return {
totalGames,
highestStage,
averageStage: Number(totalStages) / totalGames,
};
},
),
}); });

BIN
bun.lockb

Binary file not shown.

View File

@ -23,6 +23,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.59.11", "@tanstack/react-query": "^5.59.11",
"@tanstack/react-query-devtools": "^5.59.11", "@tanstack/react-query-devtools": "^5.59.11",
"@tsparticles/engine": "^3.5.0", "@tsparticles/engine": "^3.5.0",

View File

@ -9,8 +9,8 @@ interface PastMatchProps {
const PastMatch = ({ game }: PastMatchProps) => { const PastMatch = ({ game }: PastMatchProps) => {
return ( return (
<div className="flex flex-col gap-4 items-center w-full"> <div className="flex flex-col gap-4 items-center w-full @container">
<div className="w-full bg-white/10 border-white/20 border-1 rounded-md grid justify-center grid-cols-4 p-4"> <div className="w-full bg-white/10 border-white/20 border-1 rounded-md grid justify-center grid-cols-4 @max-xl:grid-cols-3 p-4">
<div className="flex-col flex"> <div className="flex-col flex">
<div className="text-white/90 text-lg">Endless</div> <div className="text-white/90 text-lg">Endless</div>
<div className="text-white/50 text-lg"> <div className="text-white/50 text-lg">
@ -24,7 +24,7 @@ const PastMatch = ({ game }: PastMatchProps) => {
{game.minesCount - game.isFlagged.flat().filter((f) => f).length} {game.minesCount - game.isFlagged.flat().filter((f) => f).length}
</div> </div>
</div> </div>
<div className="text-white/80 text-lg"> <div className="text-white/80 text-lg @max-xl:hidden">
<div>Duration: {formatTimeSpan(game.finished - game.started)}</div> <div>Duration: {formatTimeSpan(game.finished - game.started)}</div>
</div> </div>
<div className="flex justify-end"> <div className="flex justify-end">

View File

@ -0,0 +1,60 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "../lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };

View File

@ -1,20 +1,36 @@
@import "tailwindcss"; @import "tailwindcss";
@theme { @theme {
--color-primary: #D9AFD9; --color-primary: #d9afd9;
--color-input: color-mix(in srgb, var(--color-white, #fff) 20%, transparent); --color-input: color-mix(in srgb, var(--color-white, #fff) 20%, transparent);
--color-background: black; --color-background: black;
--color-common: var(--color-sky-500); --color-common: var(--color-sky-500);
--color-uncommon: var(--color-green-400); --color-uncommon: var(--color-green-400);
--color-rare: var(--color-red-500); --color-rare: var(--color-red-500);
--color-legendary: var(--color-amber-500); --color-legendary: var(--color-amber-500);
--bg-brand: -webkit-linear-gradient(225deg, rgb(251, 175, 21), rgb(251, 21, 242), --bg-brand: -webkit-linear-gradient(
rgb(21, 198, 251)) 0% 0% / 100% 300%; 225deg,
--bg-secondary: linear-gradient(90deg, #D9AFD9 0%, #97D9E1 100%) 0% 0% / 100% 300%; rgb(251, 175, 21),
rgb(251, 21, 242),
rgb(21, 198, 251)
)
0% 0% / 100% 300%;
--bg-secondary: linear-gradient(90deg, #d9afd9 0%, #97d9e1 100%) 0% 0% / 100%
300%;
--animate-gradientmove: gradientmove 1s ease 0s 1 normal forwards; --animate-gradientmove: gradientmove 1s ease 0s 1 normal forwards;
@keyframes gradientmove { @keyframes gradientmove {
0%{background-position: 0% 0%} 0% {
100%{background-position: 0% 100%} background-position: 0% 0%;
}
100% {
background-position: 0% 100%;
}
}
}
@layer components {
.bg-brand {
background: var(--bg-brand);
} }
} }
@ -22,7 +38,7 @@ button {
cursor: pointer; cursor: pointer;
} }
.grid-border-b div:not(:nth-last-child(-n+3)) { .grid-border-b div:not(:nth-last-child(-n + 3)) {
@apply border-b border-white/10; @apply border-b border-white/10;
} }

View File

@ -1,36 +1,90 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useWSQuery } from "../../hooks"; import { useWSQuery } from "../../hooks";
import dayjs from "dayjs"; import dayjs from "dayjs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/Tooltip";
import PastMatch from "../../components/PastMatch";
const Profile: React.FC = () => { const Profile: React.FC = () => {
const { data: heatmap } = useWSQuery("user.getHeatmap", { id: "Gordon" }); const { data: username } = useWSQuery("user.getSelf", null);
const now = useMemo(() => dayjs(), []); const { data: heatmap } = useWSQuery(
const firstOfYear = useMemo( "user.getHeatmap",
() => now.set("day", 0).set("month", 0).set("hour", 0).set("minute", 0), { id: username! },
[now], !!username,
); );
const { data: profile } = useWSQuery(
"user.getProfile",
{ id: username! },
!!username,
);
const { data: pastGames } = useWSQuery(
"game.getGames",
{
user: username!,
page: 0,
},
!!username,
);
const now = useMemo(() => dayjs(), []);
const firstOfYear = useMemo(() => now.startOf("year"), [now]);
const weeks = now.diff(firstOfYear, "weeks") + 1; const weeks = now.diff(firstOfYear, "weeks") + 1;
const maxHeat = heatmap ? Math.max(...heatmap) : 0; const maxHeat = heatmap ? Math.max(...heatmap) : 0;
return ( return (
<div> <div className="grid md:[grid-template-columns:_2fr_3fr] gap-6">
<div className="m-8 text-white flex self-center">
<div className="p-2 flex items-center text-2xl">{username}</div>
<div className="border-l-white border-l p-2 text-lg">
<p>Total Games: {profile?.totalGames}</p>
<p>Highest Stage: {profile?.highestStage}</p>
<p>Average Stage: {profile?.averageStage}</p>
</div>
</div>
<div className="flex flex-col gap-4">
{pastGames?.data
.slice(0, 4)
.map((game) => <PastMatch key={game.uuid} game={game} />)}
</div>
{heatmap && ( {heatmap && (
<div className="flex gap-2"> <div className="col-span-full">
<h2 className="text-white text-2xl font-semibold mb-4">Activity</h2>
<div className="flex gap-2 ">
{Array.from({ length: weeks }).map((_, w) => ( {Array.from({ length: weeks }).map((_, w) => (
<div key={w} className="w-4 flex gap-2 flex-col"> <div key={w} className="w-6 flex gap-2 flex-col">
{Array.from({ length: 7 }).map((_, d) => ( {Array.from({ length: 7 }).map((_, d) => {
<div key={d} className="w-4 h-4 border border-white"> const index = w * 7 + d;
if (index >= heatmap.length) return;
return (
<Tooltip key={d}>
<TooltipTrigger>
<div className="w-5 h-5 border border-white">
<div <div
className="w-4 h-4 bg-purple-600 -m-px" className="w-5 h-5 bg-brand -m-px"
style={{ style={{
opacity: heatmap[w * 7 + d] / maxHeat, opacity: heatmap[index] / maxHeat,
}} }}
/> />
</div> </div>
))} </TooltipTrigger>
<TooltipContent>
<p>
{firstOfYear
.clone()
.add(index, "days")
.format("DD/MM/YYYY")}
</p>
<p>{heatmap[index]} Games Played</p>
</TooltipContent>
</Tooltip>
);
})}
</div> </div>
))} ))}
</div> </div>
</div>
)} )}
</div> </div>
); );