Skip to content

Commit

Permalink
fix: improve detection of lecture visit
Browse files Browse the repository at this point in the history
  • Loading branch information
exKAZUu committed Oct 11, 2024
1 parent 36b4e59 commit 9e7367b
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 62 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@willbooster/shared-lib": "5.2.4",
"@willbooster/shared-lib-react": "3.2.9",
"@willbooster/wb": "8.0.2",
"at-decorators": "2.1.0",
"build-ts": "13.1.8",
"dayjs": "1.11.13",
"dotenv-cli": "7.4.2",
Expand Down
2 changes: 1 addition & 1 deletion src/app/(withAuth)/admin/statistics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { NextPage } from 'next';

import { logger } from '../../../../infrastructures/pino';
import { prisma } from '../../../../infrastructures/prisma';
import { Box, Heading, VStack, Table, Thead, Tbody, Tr, Th, Td } from '../../../../infrastructures/useClient/chakra';
import { Box, Heading, Table, Tbody, Td, Th, Thead, Tr, VStack } from '../../../../infrastructures/useClient/chakra';
import type { CourseId } from '../../../../problems/problemData';
import { courseIdToLectureIndexToProblemIds } from '../../../../problems/problemData';
import { dayjs } from '../../../utils/dayjs';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import type { ProblemSession, ProblemSubmission } from '@prisma/client';
import { useLocalStorage } from '@willbooster/shared-lib-react';
import NextLink from 'next/link';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { MdCheckCircle, MdCheckCircleOutline, MdOutlineVerified, MdVerified } from 'react-icons/md';

import { useAuthContextSelector } from '../../../../../../contexts/AuthContext';
import {
Box,
Card,
Expand Down Expand Up @@ -50,6 +52,15 @@ export const Lecture: React.FC<Props> = (props) => {
const completedProblemCount = lectureProblemIds.filter((problemId) => completedProblemIdSet.has(problemId)).length;
const isLessonCompleted = completedProblemCount >= lectureProblemIds.length;

const currentUserId = useAuthContextSelector((c) => c.currentUserId);
const [, setIsOpened] = useLocalStorage(
`trace-dojo.${props.params.courseId}.${props.lectureIndex}.${currentUserId}`,
false
);
useEffect(() => {
setIsOpened(true);
}, [setIsOpened]);

return (
<VStack align="stretch" spacing={6}>
<Heading as="h1">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { NextPage } from 'next';
import SuperTokensNode from 'supertokens-node';

import { logger } from '../../../../../../../../infrastructures/pino';
import { prisma } from '../../../../../../../../infrastructures/prisma';
Expand All @@ -14,8 +13,6 @@ type Props = {

const ProblemPage: NextPage<Props> = async (props) => {
const session = await getNonNullableSessionOnServer();
const superTokensUser = session && (await SuperTokensNode.getUser(session.superTokensUserId));
const isAdmin = superTokensUser?.emails[0]?.endsWith('@internet.ac.jp');

let incompleteProblemSession = await prisma.problemSession.findFirst({
where: {
Expand Down Expand Up @@ -45,7 +42,6 @@ const ProblemPage: NextPage<Props> = async (props) => {
return (
<ProblemPageOnClient
initialProblemSession={incompleteProblemSession}
isAdmin={isAdmin}
params={props.params}
userId={session.superTokensUserId}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MAX_ACTIVE_DURATION_MS_AFTER_LAST_EVENT,
MIN_INTERVAL_MS_OF_ACTIVE_EVENTS,
} from '../../../../../../../../constants';
import { useAuthContextSelector } from '../../../../../../../../contexts/AuthContext';
import { backendTrpcReact } from '../../../../../../../../infrastructures/trpcBackend/client';
import {
Button,
Expand All @@ -29,12 +30,13 @@ import { ProblemBody } from './ProblmBody';

type Props = {
initialProblemSession: ProblemSession;
isAdmin: boolean | undefined;
params: { courseId: CourseId; lectureId: string; problemId: ProblemId };
userId: string;
};

export const ProblemPageOnClient: React.FC<Props> = (props) => {
const isAdmin = useAuthContextSelector((c) => c.isAdmin);

const lectureIndex = courseIdToLectureIds[props.params.courseId].indexOf(props.params.lectureId);
const problem = useMemo(
() =>
Expand Down Expand Up @@ -127,7 +129,7 @@ export const ProblemPageOnClient: React.FC<Props> = (props) => {
: 'ステップ実行モードで最初からやり直す'}
</Button>
</Tooltip>
{props.isAdmin && (
{isAdmin && (
<Button
colorScheme="blue"
variant="outline"
Expand Down
126 changes: 76 additions & 50 deletions src/app/(withAuth)/courses/[courseId]/pageOnClient.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
'use client';

import { useLocalStorage } from '@willbooster/shared-lib-react';
import { MdOutlineVerified, MdVerified } from 'react-icons/md';

import { useAuthContextSelector } from '../../../../contexts/AuthContext';
import {
Box,
Button,
Expand Down Expand Up @@ -37,58 +39,82 @@ export const CoursePageOnClient: React.FC<Props> = (props) => {
<Heading as="h1">{courseIdToName[props.params.courseId]}</Heading>

<SimpleGrid columnGap={4} columns={{ base: 1, lg: 2 }} rowGap={6}>
{courseIdToLectureIndexToProblemIds[props.params.courseId].map((problemIds, lectureIndex) => {
const completedProblemCount = problemIds.filter((problemId) =>
props.currentUserCompletedProblemIdSet.has(problemId)
).length;
const isLectureCompleted = completedProblemCount >= problemIds.length;
{courseIdToLectureIndexToProblemIds[props.params.courseId].map((problemIds, lectureIndex) => (
<LectureCard
key={lectureIndex}
courseId={props.params.courseId}
currentUserCompletedProblemIdSet={props.currentUserCompletedProblemIdSet}
currentUserStartedProblemIdSet={props.currentUserStartedProblemIdSet}
lectureIndex={lectureIndex}
problemIds={problemIds}
/>
))}
</SimpleGrid>
</VStack>
);
};
type LectureCardProps = {
courseId: CourseId;
lectureIndex: number;
problemIds: string[];
currentUserCompletedProblemIdSet: ReadonlySet<string>;
currentUserStartedProblemIdSet: ReadonlySet<string>;
};

const isDisabled = problemIds.every((problemId) => !props.currentUserStartedProblemIdSet.has(problemId));
const url = isDisabled
? '#'
: `${props.params.courseId}/lectures/${courseIdToLectureIds[props.params.courseId][lectureIndex]}`;
const LectureCard: React.FC<LectureCardProps> = ({
courseId,
currentUserCompletedProblemIdSet,
currentUserStartedProblemIdSet,
lectureIndex,
problemIds,
}) => {
const completedProblemCount = problemIds.filter((problemId) =>
currentUserCompletedProblemIdSet.has(problemId)
).length;
const isLectureCompleted = completedProblemCount >= problemIds.length;

return (
<Card key={lectureIndex} p={2}>
<CardHeader as={HStack} gap={3}>
<Icon
as={isLectureCompleted ? MdVerified : MdOutlineVerified}
color={isLectureCompleted ? 'brand.500' : 'gray.200'}
fontSize="3xl"
mx="-0.125em"
/>
<Heading size="md">{lectureIndex + 1}</Heading>
<Spacer />
<Tooltip
isDisabled={!isDisabled}
label={`第${lectureIndex + 1}回の配布資料のURLから問題を一度開くと、ボタンが有効になります。`}
>
<Link href={url}>
<Button colorScheme="brand" isDisabled={isDisabled} mt={4}>
課題を解く
</Button>
</Link>
</Tooltip>
</CardHeader>
const currentUserId = useAuthContextSelector((c) => c.currentUserId);
const [isOpened] = useLocalStorage(`trace-dojo.${courseId}.${lectureIndex}.${currentUserId}`, false);
const isDisabled = !isOpened && problemIds.every((problemId) => !currentUserStartedProblemIdSet.has(problemId));
const url = isDisabled ? '#' : `${courseId}/lectures/${courseIdToLectureIds[courseId][lectureIndex]}`;

<CardBody align="stretch" as={VStack}>
<Progress
colorScheme="brand"
max={problemIds.length}
rounded="sm"
size="sm"
title={`${completedProblemCount}/${problemIds.length} 問題完了`}
value={completedProblemCount}
/>
<Box textAlign="right">
解答状況: {completedProblemCount}/{problemIds.length} 問 (
{Math.round((completedProblemCount / problemIds.length) * 100)}%)
</Box>
</CardBody>
</Card>
);
})}
</SimpleGrid>
</VStack>
return (
<Card p={2}>
<CardHeader as={HStack} gap={3}>
<Icon
as={isLectureCompleted ? MdVerified : MdOutlineVerified}
color={isLectureCompleted ? 'brand.500' : 'gray.200'}
fontSize="3xl"
mx="-0.125em"
/>
<Heading size="md">{lectureIndex + 1}</Heading>
<Spacer />
<Tooltip
isDisabled={!isDisabled}
label={`第${lectureIndex + 1}回の配布資料のURLから問題を一度開くと、ボタンが有効になります。`}
>
<Link href={url}>
<Button colorScheme="brand" isDisabled={isDisabled} mt={4}>
課題を解く
</Button>
</Link>
</Tooltip>
</CardHeader>

<CardBody align="stretch" as={VStack}>
<Progress
colorScheme="brand"
max={problemIds.length}
rounded="sm"
size="sm"
title={`${completedProblemCount}/${problemIds.length} 問題完了`}
value={completedProblemCount}
/>
<Box textAlign="right">
解答状況: {completedProblemCount}/{problemIds.length} 問 (
{Math.round((completedProblemCount / problemIds.length) * 100)}%)
</Box>
</CardBody>
</Card>
);
};
10 changes: 8 additions & 2 deletions src/app/(withAuth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import { SessionAuthForNextJs } from '../../components/molecules/SessionAuthForN
import { TryRefreshComponent } from '../../components/molecules/TryRefreshComponent';
import { DefaultFooter } from '../../components/organisms/DefaultFooter';
import { DefaultHeader } from '../../components/organisms/DefaultHeader';
import { AuthContextProvider } from '../../contexts/AuthContext';
import { Container, Spinner } from '../../infrastructures/useClient/chakra';
import type { LayoutProps } from '../../types';
import { getNullableSessionOnServer } from '../../utils/session';
import { getEmailFromSession, getNullableSessionOnServer } from '../../utils/session';

const DefaultLayout: NextPage<LayoutProps> = async ({ children }) => {
const { hasToken, session } = await getNullableSessionOnServer();
Expand Down Expand Up @@ -43,7 +44,12 @@ const DefaultLayout: NextPage<LayoutProps> = async ({ children }) => {
<DefaultHeader />
<Suspense fallback={<Spinner left="50%" position="fixed" top="50%" transform="translate(-50%, -50%)" />}>
<Container pb={16} pt={6}>
{children}
<AuthContextProvider
currentEmail={await getEmailFromSession(session.superTokensUserId)}
currentUserId={session.superTokensUserId}
>
{children}
</AuthContextProvider>
</Container>
</Suspense>
<DefaultFooter />
Expand Down
2 changes: 1 addition & 1 deletion src/app/(withAuth)/usage/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { NextPage } from 'next';

import { Box, Heading, VStack, Image, Text, List, ListItem } from '../../../infrastructures/useClient/chakra';
import { Box, Heading, Image, List, ListItem, Text, VStack } from '../../../infrastructures/useClient/chakra';

const CoursesPage: NextPage = async () => {
return (
Expand Down
36 changes: 36 additions & 0 deletions src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
'use client';

import React from 'react';
import { createContext, useContextSelector } from 'use-context-selector';

type Value = {
currentUserId: string;
currentEmail?: string;
isAdmin: boolean;
};

const Context = createContext<Value>(undefined as unknown as Value);

export function useAuthContextSelector<Selected>(selector: (value: Value) => Selected): Selected {
return useContextSelector(Context, selector);
}

export type Props = {
children: React.ReactNode;
currentUserId: string;
currentEmail?: string;
};

export const AuthContextProvider: React.FC<Props> = (props) => {
return (
<Context.Provider
value={{
currentUserId: props.currentUserId,
currentEmail: props.currentEmail,
isAdmin: !!props.currentEmail?.endsWith('@internet.ac.jp'),
}}
>
{props.children}
</Context.Provider>
);
};
2 changes: 1 addition & 1 deletion src/problems/instantiateProblem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Random } from '../app/utils/random';

import type { LanguageId, ProblemId } from './problemData';
import { problemIdToLanguageIdToProgram } from './problemData';
import { type TraceItemVariable, type TraceItem, traceProgram } from './traceProgram';
import { type TraceItem, type TraceItemVariable, traceProgram } from './traceProgram';

export type InstantiatedProblem = {
/**
Expand Down
19 changes: 19 additions & 0 deletions src/utils/session.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { memoizeFactory } from 'at-decorators';
import SuperTokensNode from 'supertokens-node';

import { logger } from '../infrastructures/pino';
import { prisma } from '../infrastructures/prisma';

Expand Down Expand Up @@ -48,3 +51,19 @@ async function upsertUserToPrisma(id: string): Promise<void> {
});
logger.debug('User upserted: %o', user);
}

const memoizeForEmails = memoizeFactory({
maxCachedArgsSize: Number.POSITIVE_INFINITY,
cacheDuration: 60 * 60 * 1000,
});

async function _getEmailFromSuperTokens(userId: string): Promise<string | undefined> {
try {
const user = await SuperTokensNode.getUser(userId);
return user?.emails[0];
} catch {
return undefined;
}
}

export const getEmailFromSession = memoizeForEmails(_getEmailFromSuperTokens);
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4598,6 +4598,13 @@ __metadata:
languageName: node
linkType: hard

"at-decorators@npm:2.1.0":
version: 2.1.0
resolution: "at-decorators@npm:2.1.0"
checksum: 10c0/2870952e8b99d91db6d3875f607ac35012b3937f585e52ce5640313e31d4e4417da68510594b457ffd26e51525e046c804101ad80f255681d594f1f8325e4433
languageName: node
linkType: hard

"atomic-sleep@npm:^1.0.0":
version: 1.0.0
resolution: "atomic-sleep@npm:1.0.0"
Expand Down Expand Up @@ -13902,6 +13909,7 @@ __metadata:
"@willbooster/shared-lib": "npm:5.2.4"
"@willbooster/shared-lib-react": "npm:3.2.9"
"@willbooster/wb": "npm:8.0.2"
at-decorators: "npm:2.1.0"
build-ts: "npm:13.1.8"
concurrently: "npm:9.0.1"
conventional-changelog-conventionalcommits: "npm:8.0.0"
Expand Down

0 comments on commit 9e7367b

Please sign in to comment.