Skip to content

Commit

Permalink
Feature/custom leaderboard badge (civitai#567)
Browse files Browse the repository at this point in the history
* Initial changes to let users select which leaderboard to display in creator card

* Completes custom leaderboard badge selection
  • Loading branch information
manuelurenah authored Jun 9, 2023
1 parent 7e274d1 commit deba26b
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@

-- AlterTable
ALTER TABLE "User" ADD COLUMN "leaderboardShowcase" TEXT;

-- Update UserRank view
DROP VIEW IF EXISTS "UserRank_Live";
CREATE VIEW "UserRank_Live" AS
WITH user_positions AS (
SELECT
lr."userId",
lr."leaderboardId",
l."title",
lr.position,
row_number() OVER (PARTITION BY "userId" ORDER BY "position") row_num
FROM "User" u
JOIN "LeaderboardResult" lr ON lr."userId" = u.id
JOIN "Leaderboard" l ON l.id = lr."leaderboardId" AND l.public
WHERE lr.date = current_date
AND (
u."leaderboardShowcase" IS NULL
OR lr."leaderboardId" = u."leaderboardShowcase"
)
), lowest_position AS (
SELECT
up."userId",
up.position,
up."leaderboardId",
up."title" "leaderboardTitle",
(
SELECT data->>'url'
FROM "Cosmetic" c
WHERE c."leaderboardId" = up."leaderboardId"
AND up.position <= c."leaderboardPosition"
ORDER BY c."leaderboardPosition"
LIMIT 1
) as "leaderboardCosmetic"
FROM user_positions up
WHERE row_num = 1
)
SELECT
us."userId",
lp.position "leaderboardRank",
lp."leaderboardId",
lp."leaderboardTitle",
lp."leaderboardCosmetic",
ROW_NUMBER() OVER (ORDER BY "downloadCountDay" DESC, "ratingDay" DESC, "ratingCountDay" DESC, "favoriteCountDay" DESC, us."userId") AS "downloadCountDayRank",
ROW_NUMBER() OVER (ORDER BY "favoriteCountDay" DESC, "ratingDay" DESC, "ratingCountDay" DESC, "downloadCountDay" DESC, us."userId") AS "favoriteCountDayRank",
ROW_NUMBER() OVER (ORDER BY "ratingCountDay" DESC, "ratingDay" DESC, "favoriteCountDay" DESC, "downloadCountDay" DESC, us."userId") AS "ratingCountDayRank",
ROW_NUMBER() OVER (ORDER BY "ratingDay" DESC, "ratingCountDay" DESC, "favoriteCountDay" DESC, "downloadCountDay" DESC, us."userId") AS "ratingDayRank",
ROW_NUMBER() OVER (ORDER BY "followerCountDay" DESC, "downloadCountDay" DESC, "favoriteCountDay" DESC, "ratingCountDay" DESC, us."userId") AS "followerCountDayRank",
ROW_NUMBER() OVER (ORDER BY "downloadCountWeek" DESC, "ratingWeek" DESC, "ratingCountWeek" DESC, "favoriteCountWeek" DESC, us."userId") AS "downloadCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "favoriteCountWeek" DESC, "ratingWeek" DESC, "ratingCountWeek" DESC, "downloadCountWeek" DESC, us."userId") AS "favoriteCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "ratingCountWeek" DESC, "ratingWeek" DESC, "favoriteCountWeek" DESC, "downloadCountWeek" DESC, us."userId") AS "ratingCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "ratingWeek" DESC, "ratingCountWeek" DESC, "favoriteCountWeek" DESC, "downloadCountWeek" DESC, us."userId") AS "ratingWeekRank",
ROW_NUMBER() OVER (ORDER BY "followerCountWeek" DESC, "downloadCountWeek" DESC, "favoriteCountWeek" DESC, "ratingCountWeek" DESC, us."userId") AS "followerCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "downloadCountMonth" DESC, "ratingMonth" DESC, "ratingCountMonth" DESC, "favoriteCountMonth" DESC, us."userId") AS "downloadCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "favoriteCountMonth" DESC, "ratingMonth" DESC, "ratingCountMonth" DESC, "downloadCountMonth" DESC, us."userId") AS "favoriteCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "ratingCountMonth" DESC, "ratingMonth" DESC, "favoriteCountMonth" DESC, "downloadCountMonth" DESC, us."userId") AS "ratingCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "ratingMonth" DESC, "ratingCountMonth" DESC, "favoriteCountMonth" DESC, "downloadCountMonth" DESC, us."userId") AS "ratingMonthRank",
ROW_NUMBER() OVER (ORDER BY "followerCountMonth" DESC, "downloadCountMonth" DESC, "favoriteCountMonth" DESC, "ratingCountMonth" DESC, us."userId") AS "followerCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "downloadCountYear" DESC, "ratingYear" DESC, "ratingCountYear" DESC, "favoriteCountYear" DESC, us."userId") AS "downloadCountYearRank",
ROW_NUMBER() OVER (ORDER BY "favoriteCountYear" DESC, "ratingYear" DESC, "ratingCountYear" DESC, "downloadCountYear" DESC, us."userId") AS "favoriteCountYearRank",
ROW_NUMBER() OVER (ORDER BY "ratingCountYear" DESC, "ratingYear" DESC, "favoriteCountYear" DESC, "downloadCountYear" DESC, us."userId") AS "ratingCountYearRank",
ROW_NUMBER() OVER (ORDER BY "ratingYear" DESC, "ratingCountYear" DESC, "favoriteCountYear" DESC, "downloadCountYear" DESC, us."userId") AS "ratingYearRank",
ROW_NUMBER() OVER (ORDER BY "followerCountYear" DESC, "downloadCountYear" DESC, "favoriteCountYear" DESC, "ratingCountYear" DESC, us."userId") AS "followerCountYearRank",
ROW_NUMBER() OVER (ORDER BY "downloadCountAllTime" DESC, "ratingAllTime" DESC, "ratingCountAllTime" DESC, "favoriteCountAllTime" DESC, us."userId") AS "downloadCountAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "favoriteCountAllTime" DESC, "ratingAllTime" DESC, "ratingCountAllTime" DESC, "downloadCountAllTime" DESC, us."userId") AS "favoriteCountAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "ratingCountAllTime" DESC, "ratingAllTime" DESC, "favoriteCountAllTime" DESC, "downloadCountAllTime" DESC, us."userId") AS "ratingCountAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "ratingAllTime" DESC, "ratingCountAllTime" DESC, "favoriteCountAllTime" DESC, "downloadCountAllTime" DESC, us."userId") AS "ratingAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "followerCountAllTime" DESC, "downloadCountAllTime" DESC, "favoriteCountAllTime" DESC, "ratingCountAllTime" DESC, us."userId") AS "followerCountAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "answerCountDay" DESC, "answerAcceptCountDay" DESC, us."userId") AS "answerCountDayRank",
ROW_NUMBER() OVER (ORDER BY "answerCountWeek" DESC, "answerAcceptCountWeek" DESC, us."userId") AS "answerCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "answerCountMonth" DESC, "answerAcceptCountMonth" DESC, us."userId") AS "answerCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "answerCountYear" DESC, "answerAcceptCountYear" DESC, us."userId") AS "answerCountYearRank",
ROW_NUMBER() OVER (ORDER BY "answerCountAllTime" DESC, "answerAcceptCountAllTime" DESC, us."userId") AS "answerCountAllTimeRank",
ROW_NUMBER() OVER (ORDER BY "answerAcceptCountDay" DESC, "answerCountDay" DESC, us."userId") AS "answerAcceptCountDayRank",
ROW_NUMBER() OVER (ORDER BY "answerAcceptCountWeek" DESC, "answerCountWeek" DESC, us."userId") AS "answerAcceptCountWeekRank",
ROW_NUMBER() OVER (ORDER BY "answerAcceptCountMonth" DESC, "answerCountMonth" DESC, us."userId") AS "answerAcceptCountMonthRank",
ROW_NUMBER() OVER (ORDER BY "answerAcceptCountYear" DESC, "answerCountYear" DESC, us."userId") AS "answerAcceptCountYearRank",
ROW_NUMBER() OVER (ORDER BY "answerAcceptCountAllTime" DESC, "answerCountAllTime" DESC, us."userId") AS "answerAcceptCountAllTimeRank"
FROM "UserStat" us
JOIN "User" u ON u."id" = us."userId"
LEFT JOIN lowest_position lp ON lp."userId" = us."userId";
42 changes: 22 additions & 20 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,27 @@ model SessionInvalidation {
}

model User {
id Int @id @default(autoincrement())
name String?
username String? @unique @db.Citext
email String? @unique
emailVerified DateTime?
image String?
showNsfw Boolean? @default(false)
blurNsfw Boolean? @default(true)
isModerator Boolean? @default(false)
tos Boolean? @default(false)
onboarded Boolean? @default(false)
createdAt DateTime @default(now())
deletedAt DateTime?
customerId String? @unique
subscriptionId String?
subscription CustomerSubscription?
muted Boolean? @default(false)
bannedAt DateTime?
autoplayGifs Boolean? @default(true)
filePreferences Json @default("{\"size\": \"pruned\", \"fp\": \"fp16\", \"format\": \"SafeTensor\"}")
id Int @id @default(autoincrement())
name String?
username String? @unique @db.Citext
email String? @unique
emailVerified DateTime?
image String?
showNsfw Boolean? @default(false)
blurNsfw Boolean? @default(true)
isModerator Boolean? @default(false)
tos Boolean? @default(false)
onboarded Boolean? @default(false)
createdAt DateTime @default(now())
deletedAt DateTime?
customerId String? @unique
subscriptionId String?
subscription CustomerSubscription?
muted Boolean? @default(false)
bannedAt DateTime?
autoplayGifs Boolean? @default(true)
filePreferences Json @default("{\"size\": \"pruned\", \"fp\": \"fp16\", \"format\": \"SafeTensor\"}")
leaderboardShowcase String?
accounts Account[]
sessions Session[]
Expand Down Expand Up @@ -2114,6 +2115,7 @@ model UserRank {
leaderboardRank Int?
leaderboardId String?
leaderboardTitle String?
leaderboardCosmetic String?
}

/// @view
Expand Down
57 changes: 53 additions & 4 deletions src/components/Account/SocialProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Alert, Button, Card, Center, Divider, Group, Loader, Stack, Title } from '@mantine/core';
import {
Alert,
Button,
Card,
Center,
Divider,
Group,
Loader,
Select,
Stack,
Title,
} from '@mantine/core';
import { LinkType } from '@prisma/client';
import { useSession } from 'next-auth/react';
import React, { useState } from 'react';

import { SocialLink } from '~/components/Account/SocialLink';
import { SocialLinkModal } from '~/components/Account/SocialLinkModal';
import { useCurrentUser } from '~/hooks/useCurrentUser';
import { sortDomainLinks } from '~/utils/domain-link';
import { titleCase } from '~/utils/string-helpers';
import { trpc } from '~/utils/trpc';

export function SocialProfileCard() {
const { data: session } = useSession();
const user = useCurrentUser();

const [selectedLink, setSelectedLink] = useState<{
id?: number;
Expand All @@ -19,8 +31,9 @@ export function SocialProfileCard() {

// const utils = trpc.useContext();
const { data, isLoading } = trpc.userLink.getAll.useQuery(
{ userId: session?.user?.id },
{ userId: user?.id },
{
enabled: !!user,
select: (data) => {
return {
social: data?.filter((x) => x.type === LinkType.Social),
Expand All @@ -29,6 +42,16 @@ export function SocialProfileCard() {
},
}
);
const { data: leaderboards = [], isLoading: loadingLeaderboards } =
trpc.leaderboard.getLeaderboards.useQuery();

const updateUserMutation = trpc.user.update.useMutation({
onSuccess() {
user?.refresh?.();
},
});

if (!user) return null;

const renderLinks = (type: LinkType) => {
const links = type === LinkType.Social ? data?.social : data?.sponsorship;
Expand Down Expand Up @@ -64,13 +87,39 @@ export function SocialProfileCard() {
);
};

const leaderboardOptions = leaderboards
.filter((board) => board.public)
.map(({ title, id }) => ({
label: titleCase(title),
value: id,
}));

return (
<>
<Card withBorder>
<Stack>
<Title order={2}>Creator Profile</Title>
{renderLinks(LinkType.Social)}
{renderLinks(LinkType.Sponsorship)}

<Divider label="Leaderboard Showcase" />
<Select
placeholder="Select a leaderboard"
description="Choose which leaderboard badge to display on your profile card"
name="leaderboardShowcase"
data={leaderboardOptions}
value={user.leaderboardShowcase}
onChange={(value: string | null) =>
updateUserMutation.mutate({
id: user.id,
leaderboardShowcase: value,
})
}
rightSection={updateUserMutation.isLoading ? <Loader size="xs" /> : null}
disabled={loadingLeaderboards || updateUserMutation.isLoading}
searchable={leaderboards.length > 10}
clearable
/>
</Stack>
</Card>
<SocialLinkModal selected={selectedLink} onClose={() => setSelectedLink(undefined)} />
Expand Down
8 changes: 6 additions & 2 deletions src/components/CreatorCard/CreatorCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { UserAvatar } from '~/components/UserAvatar/UserAvatar';
import { UserWithCosmetics } from '~/server/selectors/user.selector';
import { sortDomainLinks } from '~/utils/domain-link';
import { formatDate } from '~/utils/date-helpers';
import { abbreviateNumber, formatToLeastDecimals, numberWithCommas } from '~/utils/number-helpers';
import { abbreviateNumber, formatToLeastDecimals } from '~/utils/number-helpers';
import { trpc } from '~/utils/trpc';
import { StatTooltip } from '~/components/Tooltips/StatTooltip';

Expand All @@ -28,7 +28,11 @@ export function CreatorCard({ user }: Props) {
const theme = useMantineTheme();

const { data: creator } = trpc.user.getCreator.useQuery(
{ id: user.id },
{
id: user.id,
// TODO.leaderboard: uncomment when migration is done
// leaderboardId: user.leaderboardShowcase !== null ? user.leaderboardShowcase : undefined,
},
{
placeholderData: {
...user,
Expand Down
37 changes: 26 additions & 11 deletions src/components/Leaderboard/RankBadge.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import { BadgeProps, MantineSize, Text } from '@mantine/core';
import { BadgeProps, Group, MantineSize, Text } from '@mantine/core';
import { IconCrown } from '@tabler/icons-react';
import { EdgeImage } from '~/components/EdgeImage/EdgeImage';
import { IconBadge } from '~/components/IconBadge/IconBadge';

export const RankBadge = ({ rank, size, textSize = 'sm', iconSize = 18, ...props }: Props) => {
if (!rank || !rank.leaderboardRank || rank.leaderboardRank > 100) return null;

const hasLeaderboardCosmetic = !!rank.leaderboardCosmetic;

return (
<IconBadge
size={size}
tooltip={`${rank.leaderboardTitle} Rank`}
color="yellow"
href={`/leaderboard/${rank.leaderboardId}?position=${rank.leaderboardRank}`}
icon={<IconCrown size={iconSize} />}
{...props}
>
<Text size={textSize}>{rank.leaderboardRank}</Text>
</IconBadge>
<Group spacing={0} noWrap>
{rank.leaderboardCosmetic ? <EdgeImage src={rank.leaderboardCosmetic} width={32} /> : null}
<IconBadge
size={size}
tooltip={`${rank.leaderboardTitle} Rank`}
color="yellow"
// variant="outline"
href={`/leaderboard/${rank.leaderboardId}?position=${rank.leaderboardRank}`}
icon={!hasLeaderboardCosmetic ? <IconCrown size={iconSize} /> : undefined}
sx={
hasLeaderboardCosmetic
? { padding: '2px 8px', marginLeft: '-2px', height: 'auto' }
: undefined
}
{...props}
>
<Text size={textSize} inline>
#{rank.leaderboardRank}
</Text>
</IconBadge>
</Group>
);
};

Expand All @@ -24,6 +38,7 @@ type Props = {
leaderboardRank: number | null;
leaderboardId: string | null;
leaderboardTitle: string | null;
leaderboardCosmetic?: string | null;
} | null;
textSize?: MantineSize;
iconSize?: number;
Expand Down
13 changes: 11 additions & 2 deletions src/server/controllers/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
completeOnboarding,
isUsernamePermitted,
toggleUserArticleEngagement,
updateLeaderboardRank,
} from '~/server/services/user.service';
import { GetAllSchema, GetByIdInput } from '~/server/schema/base.schema';
import {
Expand Down Expand Up @@ -72,13 +73,19 @@ export const getAllUsersHandler = async ({
};

export const getUserCreatorHandler = async ({
input: { username, id },
input: { username, id, leaderboardId },
}: {
input: GetUserByUsernameSchema;
}) => {
if (!username && !id) throw throwBadRequestError('Must provide username or id');

try {
return await getUserCreator({ username, id });
const user = await getUserCreator({ username, id, leaderboardId });
if (!user) throw throwNotFoundError('Could not find user');

return user;
} catch (error) {
if (error instanceof TRPCError) throw error;
throw throwDbError(error);
}
};
Expand Down Expand Up @@ -225,6 +232,8 @@ export const updateUserHandler = async ({
},
},
});

if (data.leaderboardShowcase !== undefined) await updateLeaderboardRank(id);
if (!updatedUser) throw throwNotFoundError(`No user with id ${id}`);
if (ctx.user.showNsfw !== showNsfw) await refreshAllHiddenForUser({ userId: id });

Expand Down
21 changes: 2 additions & 19 deletions src/server/jobs/prepare-leaderboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dayjs from 'dayjs';
import { createLogger } from '~/utils/logging';
import { purgeCache } from '~/server/cloudflare/client';
import { applyDiscordLeaderboardRoles } from '~/server/jobs/apply-discord-roles';
import { updateLeaderboardRank } from '~/server/services/user.service';

const log = createLogger('leaderboard', 'blue');

Expand Down Expand Up @@ -55,25 +56,7 @@ const updateUserLeaderboardRank = createJob(
'update-user-leaderboard-rank',
'1 0 * * *',
async () => {
await dbWrite.$transaction([
dbWrite.$executeRaw`
UPDATE "UserRank" SET "leaderboardRank" = null, "leaderboardId" = null, "leaderboardTitle" = null;
`,
dbWrite.$executeRaw`
INSERT INTO "UserRank" ("userId", "leaderboardRank", "leaderboardId", "leaderboardTitle")
SELECT
"userId",
"leaderboardRank",
"leaderboardId",
"leaderboardTitle"
FROM "UserRank_Live"
ON CONFLICT ("userId") DO UPDATE SET
"leaderboardId" = excluded."leaderboardId",
"leaderboardRank" = excluded."leaderboardRank",
"leaderboardTitle" = excluded."leaderboardTitle";
`,
]);

await updateLeaderboardRank();
await applyDiscordLeaderboardRoles();
}
);
Expand Down
Loading

0 comments on commit deba26b

Please sign in to comment.