Skip to content

Commit

Permalink
feat: add course list item active style (#180)
Browse files Browse the repository at this point in the history
  • Loading branch information
zuowendong authored Mar 12, 2024
1 parent 240dbd6 commit b5b5d88
Show file tree
Hide file tree
Showing 23 changed files with 458 additions and 173 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common';
import { GlobalModule } from '../global/global.mudule';
import { GlobalModule } from '../global/global.module';
import { UserModule } from '../user/user.module';
import { AuthModule } from '../auth/auth.module';
import { CourseModule } from '../course/course.module';
Expand All @@ -10,6 +10,7 @@ import { RankModule } from '../rank/rank.module';
import { GameModule } from '../game/game.module';
import { ScheduleModule } from '@nestjs/schedule';
import { CronJobModule } from '../cron-job/cron-job.module';
import { CourseHistoryModule } from '../course-history/course-history.module';

@Module({
imports: [
Expand All @@ -22,6 +23,7 @@ import { CronJobModule } from '../cron-job/cron-job.module';
RankModule,
GameModule,
CronJobModule,
CourseHistoryModule,
RedisModule.forRootAsync({
useFactory: () => ({
type: 'single',
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/course-history/course-history.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { CourseHistoryService } from './course-history.service';
import { AuthGuard } from '../auth/auth.guard';
import { User, UserEntity } from '../user/user.decorators';

@Controller('course-history')
export class CourseHistoryController {
constructor(private readonly courseHistoryService: CourseHistoryService) {}

@UseGuards(AuthGuard)
@Get('')
courseCompletionCount(@User() user: UserEntity) {
return this.courseHistoryService.findAll(user);
}
}
10 changes: 10 additions & 0 deletions apps/api/src/course-history/course-history.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CourseHistoryService } from './course-history.service';
import { CourseHistoryController } from './course-history.controller';

@Module({
controllers: [CourseHistoryController],
providers: [CourseHistoryService],
exports: [CourseHistoryService],
})
export class CourseHistoryModule {}
60 changes: 60 additions & 0 deletions apps/api/src/course-history/course-history.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Injectable, Inject } from '@nestjs/common';
import { DB, DbType } from '../global/providers/db.provider';
import { courseHistory } from '@earthworm/shared';
import { eq, and } from 'drizzle-orm';
import { UserEntity } from 'src/user/user.decorators';

@Injectable()
export class CourseHistoryService {
constructor(@Inject(DB) private db: DbType) {}

async findOne(userId: number, courseId: number) {
return await this.db
.select()
.from(courseHistory)
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
}

async create(userId: number, courseId: number) {
await this.db.insert(courseHistory).values({
courseId,
userId,
completionCount: 1,
});
}

async updateCompletionCount(userId: number, courseId: number, count: number) {
await this.db
.update(courseHistory)
.set({
completionCount: count + 1,
})
.where(
and(
eq(courseHistory.userId, userId),
eq(courseHistory.courseId, courseId),
),
);
}

async setCompletionCount(userId: number, courseId: number) {
const result = await this.findOne(userId, courseId);
if (result && result.length) {
this.updateCompletionCount(userId, courseId, result[0].completionCount);
} else {
this.create(userId, courseId);
}
}

async findAll(user: UserEntity) {
return await this.db
.select()
.from(courseHistory)
.where(eq(courseHistory.userId, user.userId));
}
}
10 changes: 8 additions & 2 deletions apps/api/src/course/course.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common';
import { CourseService } from './course.service';
import { CourseController } from './course.controller';
import { GlobalModule } from '../global/global.mudule';
import { GlobalModule } from '../global/global.module';
import { UserProgressService } from '../user-progress/user-progress.service';
import { RankService } from '../rank/rank.service';
import { CourseHistoryService } from '../course-history/course-history.service';

@Module({
imports: [GlobalModule],
providers: [CourseService, UserProgressService, RankService],
providers: [
CourseService,
UserProgressService,
RankService,
CourseHistoryService,
],
controllers: [CourseController],
exports: [CourseService],
})
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/course/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { Injectable, Inject, HttpException, HttpStatus } from '@nestjs/common';
import { UserEntity } from 'src/user/user.decorators';
import { UserProgressService } from '../user-progress/user-progress.service';
import { RankService } from '../rank/rank.service';
import { CourseHistoryService } from '../course-history/course-history.service';

@Injectable()
export class CourseService {
constructor(
@Inject(DB) private db: DbType,
private readonly userProgressService: UserProgressService,
private readonly rankService: RankService,
private readonly courseHistoryService: CourseHistoryService,
) {}

async tryCourse() {
Expand Down Expand Up @@ -95,6 +97,7 @@ export class CourseService {
const nextCourse = await this.findNext(courseId);
await this.userProgressService.update(user.userId, nextCourse.id);
await this.rankService.userFinishCourse(user.userId, user.username);
await this.courseHistoryService.setCompletionCount(user.userId, courseId);
return nextCourse;
}
}
2 changes: 1 addition & 1 deletion apps/api/src/course/tests/course.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Test } from '@nestjs/testing';
import { MockRedisModule } from '../../../test/helper/mockRedis';
import { createUser } from '../../../test/fixture/user';
import { createFirstCourse } from '../../../test/fixture/course';
import { GlobalModule } from '../../global/global.mudule';
import { GlobalModule } from '../../global/global.module';

const user = createUser();
const course = createFirstCourse();
Expand Down
15 changes: 14 additions & 1 deletion apps/api/src/course/tests/course.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { type DbType, DB } from '../../global/providers/db.provider';
import { course, statement } from '@earthworm/shared';
import { HttpException } from '@nestjs/common';
import { createUser } from '../../../test/fixture/user';
import { GlobalModule } from '../../global/global.mudule';
import { GlobalModule } from '../../global/global.module';
import {
createFirstCourse,
createSecondCourse,
} from '../../../test/fixture/course';
import { createStatement } from '../../../test/fixture/statement';
import { cleanDB, startDB } from '../../../test/helper/utils';
import { endDB } from '../../common/db';
import { CourseHistoryService } from '../../course-history/course-history.service';

const user = createUser();
const firstCourse = createFirstCourse();
Expand All @@ -26,6 +27,7 @@ describe('course service', () => {
let courseService: CourseService;
let userProgressService: UserProgressService;
let rankService: RankService;
let courseHistoryService: CourseHistoryService;

beforeAll(async () => {
const testHelper = await setupTesting();
Expand All @@ -35,6 +37,7 @@ describe('course service', () => {
courseService = testHelper.courseService;
userProgressService = testHelper.UserProgressService;
rankService = testHelper.rankService;
courseHistoryService = testHelper.courseHistoryService;
});

afterAll(async () => {
Expand Down Expand Up @@ -106,6 +109,10 @@ describe('course service', () => {
user.userId,
user.username,
);
expect(courseHistoryService.setCompletionCount).toHaveBeenCalledWith(
user.userId,
firstCourse.id,
);
});
});

Expand All @@ -121,6 +128,9 @@ async function setupTesting() {
const mockRankService = {
userFinishCourse: jest.fn(),
};
const mockCourseHistoryService = {
setCompletionCount: jest.fn(),
};

const moduleRef = await Test.createTestingModule({
imports: [
Expand All @@ -135,6 +145,7 @@ async function setupTesting() {
CourseService,
{ provide: UserProgressService, useValue: mockUserProgressService },
{ provide: RankService, useValue: mockRankService },
{ provide: CourseHistoryService, useValue: mockCourseHistoryService },
],
}).compile();

Expand All @@ -143,6 +154,8 @@ async function setupTesting() {
UserProgressService:
moduleRef.get<UserProgressService>(UserProgressService),
rankService: moduleRef.get<RankService>(RankService),
courseHistoryService:
moduleRef.get<CourseHistoryService>(CourseHistoryService),
db: moduleRef.get<DbType>(DB),
moduleRef,
};
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/game/game.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { GameService } from './game.service';
import { GameController } from './game.controller';
import { GlobalModule } from '../global/global.mudule';
import { GlobalModule } from '../global/global.module';
import { UserProgressModule } from '../user-progress/user-progress.module';
import { CourseModule } from '../course/course.module';

Expand Down
File renamed without changes.
10 changes: 10 additions & 0 deletions apps/client/api/courseHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { http } from "./http";

export interface CourseHistory {
courseId: number;
completionCount: string;
}

export async function fetchCourseHistory() {
return await http.get<CourseHistory[], CourseHistory[]>("/course-history");
}
105 changes: 80 additions & 25 deletions apps/client/components/courses/CourseCard.vue
Original file line number Diff line number Diff line change
@@ -1,31 +1,86 @@
<template>
<div class="
flex
flex-col
w-[360px]
h-[160px]
sm:w-[500px]
md:w-[340px]
lg:w-[280px]
xl:w-[260px]
p-4
pb-6
border
border-slate-400
rounded-xl
hover:shadow-lg
hover:shadow-purple-400/50
hover:border-purple-400
transition-all
duration-500
">
<h3 class="text-xl font-bold dark:text-gray-100">{{ title }}</h3>
<p class="mt-4 truncate">this is the course's description</p>
<div
class="course-card"
:class="{
'state-finished': hasFinished,
'current-card': currentCourse,
}"
>
<h3 class="text-base font-bold dark:text-gray-100">{{ title }}</h3>
<p
class="text-sm mt-4 truncate"
:class="
currentCourse
? 'text-[rgba(255,255,255,0.8)]'
: 'text-[rgba(136,136,136,1)]'
"
>
this is the course's description
</p>

<div
v-if="hasFinished"
class="tooltip count"
:class="{
'state-finished-count': hasFinished,
'current-count': currentCourse,
}"
:data-tip="dataTip"
>
{{ count }}
</div>
</div>
</template>

<script setup lang="ts">
defineProps<{
title: string
}>()
import { computed } from "vue";
import { useActiveCourseId } from "~/composables/courses/activeCourse";
const props = defineProps<{
title: string;
id: number;
count: number | undefined;
}>();
const { activeCourseId } = useActiveCourseId();
const currentCourse = computed(() => activeCourseId.value == props.id);
const dataTip = computed(
() => `Congratulations! you've completed the course ${props.count} times.`
);
const hasFinished = computed(() => !!props.count);
</script>

<style scoped>
.course-card {
@apply flex flex-col w-[360px] h-[160px] sm:w-[500px] md:w-[340px] lg:w-[280px] xl:w-[260px] p-4 pb-6 border border-[rgba(164,175,191,1)] hover:shadow-lg hover:shadow-[rgba(164,175,191,0.5)] hover:border-[rgba(164,175,191,1)] rounded-xl transition-all duration-500 relative;
}
.count {
@apply absolute bottom-1.5 right-2 text-xs w-7 h-5 leading-5 text-center text-white bg-[rgba(164,175,191,1)] rounded-md;
}
.state-underway {
@apply border-[rgba(242,100,25,1)] hover:shadow-[rgba(242,100,25,0.5)] hover:border-[rgba(242,100,25,1)];
}
.state-underway-count {
@apply bg-[rgba(242,100,25,1)];
}
.state-finished {
@apply border-[rgba(151,71,255,1)] hover:shadow-[rgba(151,71,255,0.5)] hover:border-[rgba(151,71,255,1)];
}
.state-finished-count {
@apply bg-[rgba(151,71,255,1)];
}
.current-card {
@apply bg-[rgba(242,100,25,1)] border-[rgba(242,100,25,1)] text-white hover:shadow-[rgba(242,100,25,0.5)] hover:border-[rgba(242,100,25,1)];
}
.current-count {
@apply bg-white text-[rgba(242,100,25,1)];
}
</style>
24 changes: 24 additions & 0 deletions apps/client/composables/courses/activeCourse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ref } from "vue";

export const ACTIVE_COURSE_ID = "activeCourseId";

export function useActiveCourseId() {
const activeCourseId = ref<number>(
Number(localStorage.getItem(ACTIVE_COURSE_ID))
);

function updateActiveCourseId(id: number) {
activeCourseId.value = id;
localStorage.setItem(ACTIVE_COURSE_ID, String(id));
}

function restActiveCourseId() {
localStorage.removeItem(ACTIVE_COURSE_ID);
}

return {
activeCourseId,
restActiveCourseId,
updateActiveCourseId,
};
}
Loading

0 comments on commit b5b5d88

Please sign in to comment.