Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HomePage of User #701

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3ec8545
feat: add get User by username of API
baboon-king May 14, 2024
92c98b6
feat: view other people's homepages
baboon-king May 24, 2024
d0c9399
feat: recentCoursePacks support UserId
baboon-king May 24, 2024
69302b0
feat: finishCount support UserId
baboon-king May 24, 2024
3ce1cb3
feat: home page redirect to user's HomePage
baboon-king May 24, 2024
902ea2f
feat: RecentCoursePack fellow username
baboon-king May 24, 2024
cb3e464
chore: merge code
baboon-king May 24, 2024
818ef53
feat: CalendarGraph data follow username
baboon-king May 24, 2024
b757165
chore: RankingList to homepage of user
baboon-king May 24, 2024
70acc89
refactor: /user/:username to /:username
baboon-king May 25, 2024
009b87c
chore: remove /recent-course-packs-by-user-id API
baboon-king May 25, 2024
7d3944b
chore: remove finishCountByUserId API
baboon-king May 25, 2024
642247e
chore: optimize code of isSelf
baboon-king May 25, 2024
600dd08
feat: open new tab for other users
baboon-king May 25, 2024
eaa2463
feat: add tips for username on ranking list
baboon-king May 25, 2024
0b58033
fix: error
baboon-king May 25, 2024
d52574e
chore: remove Invalid code
baboon-king May 25, 2024
1d7afd3
feat: hide 更多课程包 on not self
baboon-king May 25, 2024
1e5f563
feat: current user homepage on root path
baboon-king May 25, 2024
82af7a3
fix: test error of recent-course-packs
baboon-king May 25, 2024
550affb
chore: remove log
baboon-king May 25, 2024
19d574a
fix: unexpected refetch of learn records
baboon-king May 25, 2024
b39d96d
feat: debounced of LearnRecord
baboon-king May 25, 2024
a0660bb
feat: merge code of Avatar
baboon-king May 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ describe("user-progress e2e", () => {
const courseIdFirst = createId();
const coursePackIdSecond = createId();
const courseIdSecond = createId();
await insertUserCourseProgress(db, coursePackIdFirst, courseIdFirst, 0);
await insertUserCourseProgress(db, coursePackIdSecond, courseIdSecond, 10);
const userId = createId();
await insertUserCourseProgress(db, coursePackIdFirst, courseIdFirst, 0, userId);
await insertUserCourseProgress(db, coursePackIdSecond, courseIdSecond, 10, userId);

await request(app.getHttpServer())
.get("/user-course-progress/recent-course-packs")
.set("Authorization", `Bearer ${token}`)
.query({ userId })
.expect(200)
.expect(({ body }) => {
expect(body.length).toBe(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import { UserCourseProgressService } from "./user-course-progress.service";
export class UserProgressController {
constructor(private readonly userCourseProgressService: UserCourseProgressService) {}

@UseGuards(AuthGuard)
@Get("/recent-course-packs")
async getUserRecentCoursePacks(@User() user: UserEntity, @Query("limit") limit: number) {
async getUserRecentCoursePacks(@Query("userId") userId?: string, @Query("limit") limit?: number) {
const recentCoursePacks = await this.userCourseProgressService.getUserRecentCoursePacks(
user.userId,
userId,
limit || 3,
);
return recentCoursePacks;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import { UserLearnRecordService } from "./user-learn-record.service";
export class UserLearnRecordController {
constructor(private userLearnRecordService: UserLearnRecordService) {}

@UseGuards(AuthGuard)
@Get("finishCount")
finishCount(@User() user: UserEntity, @Query() dto?: GetUserLearnRecordDto) {
return this.userLearnRecordService.find(user.userId, dto);
finishCount(@Query("userId") userId: string, @Query() dto?: GetUserLearnRecordDto) {
return this.userLearnRecordService.find(userId, dto);
}
}
7 changes: 6 additions & 1 deletion apps/api/src/user/user.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
import { Body, Controller, Get, Param, Patch, Post, UseGuards } from "@nestjs/common";

import { AuthGuard } from "../guards/auth.guard";
import { User, UserEntity } from "../user/user.decorators";
Expand All @@ -21,6 +21,11 @@ export class UserController {
return userInfo;
}

@Get(":username")
getUserByUsername(@Param("username") username: string) {
return this.userService.getUserByUsername(username);
}

// 给新用户第一次登录使用
// 目前使用 email 和 github 登录的用户 都不存在 username
// 所以这个接口有两个目的
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ export class UserService {
throw new HttpException(e.response.data.message, e.response.status);
}
}
async getUserByUsername(username: string) {
const params = new URLSearchParams([["search.username", username]]).toString();

try {
const res = await this.logtoService.logtoApi.get(`/api/users/?${params}`);
if (res.status === 200) {
const user = res.data.at(0);
return user;
} else {
return res.data;
}
} catch (e) {
throw new HttpException(e.response.data.message, e.response.status);
}
}

async setupNewUser(user: UserEntity, dto: { username: string; avatar: string }) {
if (!dto.avatar) {
Expand Down
3 changes: 2 additions & 1 deletion apps/api/test/fixture/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ export async function insertUserCourseProgress(
coursePackId: string,
courseId: string,
statementIndex: number,
userId?: string,
) {
const [entity] = await db
.insert(userCourseProgress)
.values({
userId: getTokenOwner(),
userId: userId || getTokenOwner(),
coursePackId,
courseId,
statementIndex,
Expand Down
4 changes: 4 additions & 0 deletions apps/client/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export async function fetchCurrentUser() {
avatar: logtoUserInfo!.picture || "", // 添加 avatar 字段,默认值为 picture ( picture 这个属性不够清晰 不喜欢)
} as User;
}
export async function getUserByUsername(username: string) {
// `/user/username/${username}` is better ?
return await http.get<unknown, any>(`/user/${username}`);
}
10 changes: 8 additions & 2 deletions apps/client/api/userCourseProgress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export async function fetchUpdateCourseProgress(userProgressUpdate: UserProgress
);
}

export async function fetchUserRecentCoursePacks() {
return await http.get<UserRecentCoursePackResponse[], UserRecentCoursePackResponse[]>(
export async function fetchUserRecentCoursePacks(userId: string, limit = 4) {
return await http.get<unknown, UserRecentCoursePackResponse[]>(
`/user-course-progress/recent-course-packs`,
{
params: {
userId,
limit,
},
},
);
}
11 changes: 4 additions & 7 deletions apps/client/api/userLearnRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ export interface UserLearnRecordResponse {
list: Array<{ day: string; count: number }>;
}

export async function fetchLearnRecord(params: UserLearnRecord) {
return await http.get<UserLearnRecordResponse, UserLearnRecordResponse>(
`/user-learn-record/finishCount`,
{
params,
},
);
export async function fetchLearnRecord(params: UserLearnRecord & { userId: string }) {
return await http.get<unknown, UserLearnRecordResponse>(`/user-learn-record/finishCount`, {
params,
});
}
18 changes: 16 additions & 2 deletions apps/client/components/rank/RankingItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,29 @@
class="w-16"
:rank="rank"
/>
<div class="flex-1 truncate text-center">{{ username || "匿名" }}</div>
<div
class="tooltip flex-1 cursor-pointer truncate text-center"
data-tippy-content="看看这小子最近学了啥"
@click="toUserHomePage"
@mouseenter="$lazyTippy"
>
{{ username || "匿名" }}
</div>
<div class="w-16 text-right">{{ count }} 课</div>
</div>
</template>

<script setup lang="ts">
defineProps({
import { useRouter } from "nuxt/app";

const props = defineProps({
rank: Number,
username: String,
count: Number,
});
const router = useRouter();
const toUserHomePage = () => {
const { href } = router.resolve(`/${props.username}`);
window.open(href, "_blank");
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
<p class="my-2 line-clamp-2 min-h-[3em] text-sm text-gray-500">
{{ coursePack.description }}
</p>
<div class="flex justify-between">
<div
v-if="isSelf"
class="flex justify-between"
>
<button
class="btn btn-sm tw-btn-blue"
@click="handleGotoCourseList(coursePack.coursePackId)"
Expand Down Expand Up @@ -61,11 +64,20 @@

<script setup lang="ts">
import { navigateTo } from "#app";
import { ref } from "vue";
import { computed, ref } from "vue";

import { useUserStore } from "~/store/user";
import { useRecentCoursePack } from "./helper";

const { coursePacks, fetchCoursePacks } = useRecentCoursePack();
const props = defineProps<{
userId: string;
}>();

const { coursePacks, fetchCoursePacks } = useRecentCoursePack({ userId: props.userId });

const userStore = useUserStore();

const isSelf = userStore.isSelf(props.userId);

const isLoading = ref(false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import { fetchUserRecentCoursePacks } from "~/api/userCourseProgress";

const coursePacks = ref<UserRecentCoursePackResponse[]>([]);

export function useRecentCoursePack() {
interface UseRecentCoursePackOptions {
userId: string;
limit?: number;
}

export function useRecentCoursePack(options: UseRecentCoursePackOptions) {
const { userId, limit = 4 } = options || {};

async function fetchCoursePacks() {
coursePacks.value = await fetchUserRecentCoursePacks();
coursePacks.value = await fetchUserRecentCoursePacks(userId, limit);
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,13 @@
<!-- 通过给定高度来自适应拉伸图片,如果图片不存在或者加载失败则显示外层的背景色(没有宽度) -->
<img
class="h-full object-cover"
:src="userStore.user?.avatar"
:src="user?.avatar"
/>
</div>
<div class="mt-4 truncate">
<div class="flex gap-2">
<div class="text-3xl font-medium">{{ userStore.user?.username }}</div>
<MembershipBadge></MembershipBadge>
</div>
<div class="text-3xl font-medium">{{ user?.username }}</div>
<div class="text-md text-gray-400">
{{ userStore.user?.name }}
{{ user?.name }}
</div>
</div>
<hr class="my-5 dark:border-gray-700" />
Expand All @@ -36,47 +33,46 @@
<div class="mb-4 flex justify-between border-b pb-2 dark:border-gray-700">
<div class="text-xl font-medium">最近使用的课程包</div>
<NuxtLink
v-if="isSelf"
href="/course-pack"
class="link text-blue-500 no-underline hover:opacity-75"
>更多课程包
>
更多课程包
</NuxtLink>
</div>
<HomeRecentCoursePack />
<HomeCalendarGraph
<RecentCoursePack :userId="user?.id" />
<CalendarGraph
class="mt-10"
:data="learnRecord.list"
:totalCount="learnRecord.totalCount"
@toggleYear="toggleYear"
@toggleYear="onToggleYear"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";

<script setup lang="ts">
import { getUserByUsername } from "~/api/user";
import { useLearnRecord } from "~/composables/learnRecord";
import { type CalendarData } from "~/composables/user/calendarGraph";
import { useUserStore } from "~/store/user";
import CalendarGraph from "./CalendarGraph.vue";
import RecentCoursePack from "./RecentCoursePack.vue";

const userStore = useUserStore();
const { learnRecord, setupLearnRecord, setQueryYear } = useLearnRecord();
const { toggleYear } = useCalendarGraph();
const props = defineProps<{
username: string;
}>();

function useCalendarGraph() {
const data = ref<CalendarData[]>([]);
const totalCount = ref<number>(0);
const user = await getUserByUsername(props.username);

async function toggleYear(year?: number) {
setQueryYear(year);
setupLearnRecord();
}
const isSelf = useUserStore().isSelf(() => user?.id);
const { learnRecord, year } = useLearnRecord({ userId: user?.id });

return {
data,
totalCount,
toggleYear,
};
}
const onToggleYear = (value?: number) => {
if (!value) {
return;
}
year.value = value!;
};
</script>

<style scoped></style>
64 changes: 38 additions & 26 deletions apps/client/composables/learnRecord.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,58 @@
import { ref } from "vue";
import type { MaybeRef } from "vue";

import { refDebounced } from "@vueuse/core";
import { ref, toValue, watch } from "vue";

import type { UserLearnRecordResponse } from "~/api/userLearnRecord";
import { fetchLearnRecord } from "~/api/userLearnRecord";
import { useUserStore } from "~/store/user";

let year: number | undefined = 0;
const learnRecord = ref<UserLearnRecordResponse>({
list: [],
totalCount: 0,
});
let isSetup = false;

export function useLearnRecord() {
function setQueryYear(val?: number) {
if (year !== val) {
year = val;
isSetup = false;
}
}
interface UseLearnRecordOptions {
year?: MaybeRef<number>;
/**
* @default string current user
*/
userId?: string;
}

export function useLearnRecord(options: UseLearnRecordOptions) {
const userStore = useUserStore();
const { userId = userStore.userInfo?.sub! } = options || {};

const learnRecord = ref<UserLearnRecordResponse>({
list: [],
totalCount: 0,
});

const year = ref(options?.year || new Date().getFullYear());
const debouncedYear = refDebounced(year, 1500);

function getQuery() {
const yearStr = toValue(year);
return {
startDate: year ? `${year}-01-01` : undefined,
endDate: year ? `${year}-12-31` : undefined,
userId,
startDate: yearStr ? `${yearStr}-01-01` : undefined,
endDate: yearStr ? `${yearStr}-12-31` : undefined,
};
}

async function updateLearnRecord() {
const res = await fetchLearnRecord(getQuery());
learnRecord.value = res;
}

async function setupLearnRecord() {
if (isSetup) return;
isSetup = true;
const res = await fetchLearnRecord(getQuery());
learnRecord.value = res;
}
watch(
debouncedYear,
() => {
updateLearnRecord();
},
{
immediate: true,
},
);

return {
year,
learnRecord,
updateLearnRecord,
setQueryYear,
setupLearnRecord,
};
}
Loading
Loading