Skip to content

Commit 7b55d07

Browse files
committed
feat: add Codeforces section to user profile
1 parent dbe0d45 commit 7b55d07

5 files changed

Lines changed: 91 additions & 1 deletion

File tree

backend/src/controllers/user.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import type { UserProfileQueryResult } from '@putongoj/shared'
12
import type { Context } from 'koa'
23
import type { UserDocument } from '../models/User'
34
import {
5+
OAuthProvider,
46
UserItemListQueryResultSchema,
57
UserProfileQueryResultSchema,
68
UserRanklistExportQueryResultSchema,
@@ -15,6 +17,7 @@ import config from '../config'
1517
import { loadProfile } from '../middlewares/authn'
1618
import Group from '../models/Group'
1719
import Solution from '../models/Solution'
20+
import oauthService from '../services/oauth'
1821
import userService from '../services/user'
1922
import {
2023
createEnvelopedResponse,
@@ -87,9 +90,19 @@ export async function getUser (ctx: Context) {
8790
.lean(),
8891
])
8992

93+
let codeforces: UserProfileQueryResult['codeforces'] = null
94+
const codeforcesConnection = await oauthService.getUserOAuthConnection(
95+
user._id, OAuthProvider.Codeforces)
96+
if (codeforcesConnection?.raw?.handle && codeforcesConnection?.raw?.rating) {
97+
const { handle, rating } = codeforcesConnection.raw
98+
if (typeof handle === 'string' && typeof rating === 'number') {
99+
codeforces = { handle, rating }
100+
}
101+
}
102+
90103
const attempted = difference(failed, solved)
91104
const result = UserProfileQueryResultSchema.encode({
92-
...user.toObject(), groups, solved, attempted,
105+
...user.toObject(), groups, solved, attempted, codeforces,
93106
})
94107
return createEnvelopedResponse(ctx, result)
95108
}

backend/src/services/oauth.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,14 @@ export async function findUserByOAuthConnection (
315315
return oauthRecord?.user as UserDocument ?? null
316316
}
317317

318+
export async function getUserOAuthConnection (
319+
userId: Types.ObjectId,
320+
provider: OAuthProvider,
321+
): Promise<OAuthDocument | null> {
322+
const record = await OAuth.findOne({ user: userId, provider }) as OAuthDocument | null
323+
return record
324+
}
325+
318326
export async function getUserOAuthConnections (
319327
userId: Types.ObjectId,
320328
): Promise<Record<OAuthProvider, OAuthDocument | null>> {
@@ -383,6 +391,7 @@ const oauthService = {
383391
generateOAuthUrl,
384392
handleOAuthCallback,
385393
findUserByOAuthConnection,
394+
getUserOAuthConnection,
386395
getUserOAuthConnections,
387396
isOAuthConnectedToAnotherUser,
388397
upsertOAuthConnection,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<script setup lang="ts">
2+
import { computed } from 'vue'
3+
4+
const props = defineProps<{
5+
handle: string
6+
rating: number
7+
}>()
8+
9+
const codeforcesTitle = computed(() => {
10+
if (props.rating >= 4000) return props.handle
11+
if (props.rating >= 3000) return 'Legendary Grandmaster'
12+
if (props.rating >= 2600) return 'International Grandmaster'
13+
if (props.rating >= 2400) return 'Grandmaster'
14+
if (props.rating >= 2300) return 'International Master'
15+
if (props.rating >= 2100) return 'Master'
16+
if (props.rating >= 1900) return 'Candidate Master'
17+
if (props.rating >= 1600) return 'Expert'
18+
if (props.rating >= 1400) return 'Specialist'
19+
if (props.rating >= 1200) return 'Pupil'
20+
return 'Newbie'
21+
})
22+
</script>
23+
24+
<template>
25+
<div
26+
class="font-(family-name:--font-verdana) px-3.5 py-2" :class="{
27+
'text-color': props.rating >= 4000,
28+
'text-red-500': props.rating >= 2400 && props.rating < 4000,
29+
'text-amber-500': props.rating >= 2000 && props.rating < 2400,
30+
'text-fuchsia-600': props.rating >= 1800 && props.rating < 2000,
31+
'text-blue-600': props.rating >= 1600 && props.rating < 1800,
32+
'text-teal-500': props.rating >= 1400 && props.rating < 1600,
33+
'text-green-700': props.rating >= 1200 && props.rating < 1400,
34+
'text-neutral-500': props.rating < 1200,
35+
}"
36+
>
37+
<div class="font-semibold text-sm">
38+
{{ codeforcesTitle }}
39+
</div>
40+
<a
41+
:href="`https://codeforces.com/profile/${props.handle}`" target="_blank" rel="noopener noreferrer"
42+
class="font-sans font-semibold hover:underline text-2xl text-inherit"
43+
>
44+
<template v-if="props.rating >= 3000">
45+
<span
46+
:class="{
47+
'text-red-500': props.rating >= 4000,
48+
'text-color': props.rating >= 3000 && props.rating < 4000,
49+
}"
50+
>{{ props.handle.slice(0, 1) }}</span>{{ props.handle.slice(1) }}
51+
</template>
52+
<template v-else>{{ props.handle }}</template>
53+
</a>
54+
<div class="mt-1">
55+
<span class="mr-1 text-color">Contest rating:</span>
56+
<span class="font-(family-name:--font-verdana) font-bold"> {{ props.rating }}</span>
57+
</div>
58+
</div>
59+
</template>

frontend/src/views/UserProfile.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { computed, onMounted, ref } from 'vue'
99
import { useI18n } from 'vue-i18n'
1010
import { useRoute, useRouter } from 'vue-router'
1111
import { getUser } from '@/api/user'
12+
import CodeforcesProfile from '@/components/CodeforcesProfile.vue'
1213
import { useRootStore } from '@/store'
1314
import { useSessionStore } from '@/store/modules/session'
1415
import { getPrivilegeLabel, getPrivilegeSeverity, timePretty } from '@/utils/formate'
@@ -170,6 +171,10 @@ onRouteParamUpdate(fetch)
170171
</div>
171172
</Fieldset>
172173

174+
<Fieldset v-if="user.codeforces" :legend="t('ptoj.codeforces')">
175+
<CodeforcesProfile :handle="user.codeforces.handle" :rating="user.codeforces.rating" />
176+
</Fieldset>
177+
173178
<Fieldset :legend="t('ptoj.statistics')">
174179
<div class="gap-4 grid grid-cols-2">
175180
<div class="px-4 py-2">

shared/src/types/api/user.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export const UserProfileQueryResultSchema = z.object({
1616
gid: GroupModelSchema.shape.gid,
1717
title: GroupModelSchema.shape.title,
1818
})),
19+
codeforces: z.object({
20+
handle: z.string(),
21+
rating: z.number(),
22+
}).nullable(),
1923
solved: z.array(z.number()),
2024
attempted: z.array(z.number()),
2125
createdAt: UserModelSchema.shape.createdAt,

0 commit comments

Comments
 (0)