Skip to content

Commit

Permalink
Update leaderboard queries to use kysely, fix light theme flash
Browse files Browse the repository at this point in the history
  • Loading branch information
tzushimelon committed Jan 9, 2024
1 parent 3d4a250 commit 79efa4d
Show file tree
Hide file tree
Showing 30 changed files with 438 additions and 246 deletions.
9 changes: 7 additions & 2 deletions app/components/ColorModeToggle/ColorModeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { ButtonProps } from "@chakra-ui/react";
import { Button, Flex, useColorMode } from "@chakra-ui/react";
import { BsSun, BsMoonStarsFill } from "react-icons/bs";
import { BsSun, BsMoonStarsFill } from "react-icons/bs/index.js";

export default function ColorModeToggle(props: ButtonProps) {
const { colorMode, toggleColorMode } = useColorMode();

const toggle = () => {
toggleColorMode();
console.log("toggling colorMode", colorMode);
};

return (
/**
* Ideally, only the button component should be used (without Flex).
Expand All @@ -13,7 +18,7 @@ export default function ColorModeToggle(props: ButtonProps) {

<Button
aria-label="Toggle Color Mode"
onClick={toggleColorMode}
onClick={toggle}
_focus={{ boxShadow: "none" }}
w="fit-content"
{...props}
Expand Down
2 changes: 1 addition & 1 deletion app/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Link as ChakraLink,
} from "@chakra-ui/react";
import { Link } from "@remix-run/react";
import { FaDiscord, FaGithub } from "react-icons/fa";
import { FaDiscord, FaGithub } from "react-icons/fa/index.js";

export default function Footer() {
return (
Expand Down
2 changes: 1 addition & 1 deletion app/components/Leaderboard/Leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function Leaderboard({ items }: LeaderboardProps) {
return (
<Box>
{items.map((item, index) => (
<LeaderboardUser key={item.username} {...item} />
<LeaderboardUser key={item.userId} {...item} />
))}
</Box>
);
Expand Down
21 changes: 8 additions & 13 deletions app/components/Leaderboard/LeaderboardUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export interface LeaderboardUserProps {
userId: string;
rank: number;
username: string;
avatarHash?: string;
discriminator: string;
level: string;
avatarUrl: string | null;
discriminator: number;
level: number;
xpProgress: number;
xpTotal: number;
}
Expand All @@ -23,7 +23,7 @@ export default function LeaderboardUser({
userId,
rank,
username,
avatarHash,
avatarUrl,
discriminator,
level,
xpProgress,
Expand Down Expand Up @@ -55,19 +55,14 @@ export default function LeaderboardUser({
<Avatar
size="lg"
src={
avatarHash
? `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.webp?size=4096`
avatarUrl
? avatarUrl
: `https://cdn.discordapp.com/embed/avatars/${
parseInt(discriminator) % 5
parseInt(userId) % 5
}.png`
}
/>
<Text>
{username}
<Text as="span" color={useColorModeValue("gray.300", "gray.500")}>
#{discriminator}
</Text>
</Text>
<Text>{username}</Text>
</HStack>
</Box>
<VStack width="full" alignItems="start">
Expand Down
7 changes: 6 additions & 1 deletion app/components/Navbar/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import {
Spacer,
useOutsideClick,
} from "@chakra-ui/react";
import { FiTrendingUp, FiBook, FiCommand, FiMenu } from "react-icons/fi";
import {
FiTrendingUp,
FiBook,
FiCommand,
FiMenu,
} from "react-icons/fi/index.js";
import ColorModeToggle from "../ColorModeToggle/ColorModeToggle";
import { Link, NavLink, useMatches } from "@remix-run/react";
import SushiiIcon from "../../images/sushii.png";
Expand Down
2 changes: 1 addition & 1 deletion app/components/Statistics/Staticstic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
useColorModeValue,
} from "@chakra-ui/react";
import type { IconType } from "react-icons";
import { FiHome, FiCommand, FiUsers } from "react-icons/fi";
import { FiHome, FiCommand, FiUsers } from "react-icons/fi/index.js";

const ICON_MAP: Record<string, IconType> = {
guild_count: FiHome,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Kysely } from "kysely";
import type { DB } from "../types";
import type { BotStatRow } from "./BotStat.table";
import type { BotStatRow } from "./BotStat.table.server";

export function getAllBotStats(db: Kysely<DB>): Promise<BotStatRow[]> {
return db.selectFrom("app_public.bot_stats").selectAll().execute();
Expand Down
File renamed without changes.
215 changes: 215 additions & 0 deletions app/db/Leaderboard/Leaderboard.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { sql, type QueryCreator } from "kysely";
import type { DB } from "../types";

type TimeFrame = "day" | "week" | "month" | "all_time";

export interface UserRank {
userId: string;
rank: number;
xpTotal: string | number | bigint;

// Nullable since user might not be cached
username: string | null;
avatarUrl: string | null;
discriminator: number | null;
}

export function getGlobalRanks(
db: QueryCreator<DB>,
timeframe: TimeFrame,
limit: number,
offset: number
): Promise<UserRank[]> {
let query = db
// Subquery to get the total XP for each user
.selectFrom((eb) =>
eb
.selectFrom("app_public.user_levels")
.select(({ fn }) => [
"user_id",
fn.sum("msg_day").as("msg_day"),
fn.sum("msg_week").as("msg_week"),
fn.sum("msg_month").as("msg_month"),
fn.sum("msg_all_time").as("msg_all_time"),
])
.groupBy("user_id")
.as("global_xp")
)
.leftJoin("app_public.cached_users", (join) =>
join.onRef("user_id", "=", "id")
)
.select(({ fn }) => [
"user_id as userId",
"avatar_url as avatarUrl",
"name as username",
"discriminator",

// Rank stuff
"msg_all_time as xpTotal",
fn
.agg<number>("row_number")
.over((ob) => {
switch (timeframe) {
case "day":
return ob.orderBy("msg_day", "desc");
case "week":
return ob.orderBy("msg_week", "desc");
case "month":
return ob.orderBy("msg_month", "desc");
case "all_time":
return ob.orderBy("msg_all_time", "desc");
}
})
.as("rank"),
])
.limit(limit)
.offset(offset);

switch (timeframe) {
case "day": {
query = query
.where(
sql`extract(doy from last_msg)`,
"=",
sql`extract(doy from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_day", "desc");
}
case "week": {
query = query
.where(
sql`extract(week from last_msg)`,
"=",
sql`extract(week from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_week", "desc");
}
case "month": {
query = query
.where(
sql`extract(month from last_msg)`,
"=",
sql`extract(month from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_month", "desc");
}
case "all_time": {
query = query.orderBy("msg_all_time", "desc");
}
}

return query.execute();
}

/**
* Get the ranks and XP of all users in a timeframe.
*
* @param db database
* @param guildId guild ID
* @param timeframe
*/
export function getGuildRanks(
db: QueryCreator<DB>,
guildId: string,
timeframe: TimeFrame,
limit: number,
offset: number
): Promise<UserRank[]> {
let query = db
.selectFrom("app_public.user_levels")
.leftJoin("app_public.cached_users", (join) =>
join.onRef("user_id", "=", "id")
)
.select(({ fn }) => [
"user_id as userId",
"avatar_url as avatarUrl",
"name as username",
"discriminator",

// Rank stuff
"msg_all_time as xpTotal",
fn
.agg<number>("row_number")
.over((ob) => {
switch (timeframe) {
case "day":
return ob.orderBy("msg_day", "desc");
case "week":
return ob.orderBy("msg_week", "desc");
case "month":
return ob.orderBy("msg_month", "desc");
case "all_time":
return ob.orderBy("msg_all_time", "desc");
}
})
.as("rank"),
])
.where("guild_id", "=", guildId)
.limit(limit)
.offset(offset);

switch (timeframe) {
case "day": {
query = query
.where(
sql`extract(doy from last_msg)`,
"=",
sql`extract(doy from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_day", "desc");
}
case "week": {
query = query
.where(
sql`extract(week from last_msg)`,
"=",
sql`extract(week from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_week", "desc");
}
case "month": {
query = query
.where(
sql`extract(month from last_msg)`,
"=",
sql`extract(month from now())`
)
.where(
sql`extract(year from last_msg)`,
"=",
sql`extract(year from now())`
)
.orderBy("msg_month", "desc");
}
case "all_time": {
query = query.orderBy("msg_all_time", "desc");
}
}

return query.execute();
}
6 changes: 6 additions & 0 deletions app/db/Leaderboard/UserLevel.table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Insertable, Selectable, Updateable } from "kysely";
import type { AppPublicUserLevels } from "../types";

export type UserLevelRow = Selectable<AppPublicUserLevels>;
export type InsertableUserLevelRow = Insertable<AppPublicUserLevels>;
export type UpdateableUserLevelRow = Updateable<AppPublicUserLevels>;
9 changes: 5 additions & 4 deletions app/database.server.ts → app/db/database.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Pool } from "pg";
import config from "./config";
import logger from "./logger";
import pg from "pg";
import config from "../config";
import logger from "../logger";
import { Kysely, PostgresDialect } from "kysely";
import Cursor from "pg-cursor";
import type { DB } from "./db/types";
import type { DB } from "./types";

const dbLogger = logger.child({ component: "db" });

const { Pool } = pg;
const pool = new Pool({
connectionString: config.DATABASE_URL,
// Don't really need that many connections
Expand Down
Loading

0 comments on commit 79efa4d

Please sign in to comment.