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 { emit } from "../events";
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";
const secret = process.env.SECRET!;
@ -185,11 +185,7 @@ export const userController = createController({
}),
async ({ id }, { db }) => {
const now = dayjs();
const firstOfYear = now
.set("day", 0)
.set("month", 0)
.set("hour", 0)
.set("minute", 0);
const firstOfYear = now.startOf("year");
const gamesOfUser = await db.query.Game.findMany({
where: and(eq(Game.user, id), gt(Game.finished, firstOfYear.valueOf())),
});
@ -203,4 +199,34 @@ export const userController = createController({
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-popover": "^1.1.2",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-query": "^5.59.11",
"@tanstack/react-query-devtools": "^5.59.11",
"@tsparticles/engine": "^3.5.0",

View File

@ -9,8 +9,8 @@ interface PastMatchProps {
const PastMatch = ({ game }: PastMatchProps) => {
return (
<div className="flex flex-col gap-4 items-center w-full">
<div className="w-full bg-white/10 border-white/20 border-1 rounded-md grid justify-center grid-cols-4 p-4">
<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 @max-xl:grid-cols-3 p-4">
<div className="flex-col flex">
<div className="text-white/90 text-lg">Endless</div>
<div className="text-white/50 text-lg">
@ -24,7 +24,7 @@ const PastMatch = ({ game }: PastMatchProps) => {
{game.minesCount - game.isFlagged.flat().filter((f) => f).length}
</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>
<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";
@theme {
--color-primary: #D9AFD9;
--color-primary: #d9afd9;
--color-input: color-mix(in srgb, var(--color-white, #fff) 20%, transparent);
--color-background: black;
--color-common: var(--color-sky-500);
--color-uncommon: var(--color-green-400);
--color-rare: var(--color-red-500);
--color-legendary: var(--color-amber-500);
--bg-brand: -webkit-linear-gradient(225deg, 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%;
--bg-brand: -webkit-linear-gradient(
225deg,
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;
@keyframes gradientmove {
0%{background-position: 0% 0%}
100%{background-position: 0% 100%}
0% {
background-position: 0% 0%;
}
100% {
background-position: 0% 100%;
}
}
}
@layer components {
.bg-brand {
background: var(--bg-brand);
}
}
@ -22,7 +38,7 @@ button {
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;
}

View File

@ -1,36 +1,90 @@
import { useMemo } from "react";
import { useWSQuery } from "../../hooks";
import dayjs from "dayjs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "../../components/Tooltip";
import PastMatch from "../../components/PastMatch";
const Profile: React.FC = () => {
const { data: heatmap } = useWSQuery("user.getHeatmap", { id: "Gordon" });
const now = useMemo(() => dayjs(), []);
const firstOfYear = useMemo(
() => now.set("day", 0).set("month", 0).set("hour", 0).set("minute", 0),
[now],
const { data: username } = useWSQuery("user.getSelf", null);
const { data: heatmap } = useWSQuery(
"user.getHeatmap",
{ id: username! },
!!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 maxHeat = heatmap ? Math.max(...heatmap) : 0;
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 && (
<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) => (
<div key={w} className="w-4 flex gap-2 flex-col">
{Array.from({ length: 7 }).map((_, d) => (
<div key={d} className="w-4 h-4 border border-white">
<div key={w} className="w-6 flex gap-2 flex-col">
{Array.from({ length: 7 }).map((_, d) => {
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
className="w-4 h-4 bg-purple-600 -m-px"
className="w-5 h-5 bg-brand -m-px"
style={{
opacity: heatmap[w * 7 + d] / maxHeat,
opacity: heatmap[index] / maxHeat,
}}
/>
</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>
);