Skip to content

Commit 061bff9

Browse files
authored
Merge pull request #1283 from isaacphysics/redesign/my-assignments
Redesign My Assignments page
2 parents 40b2d40 + 55c0cfa commit 061bff9

22 files changed

+406
-117
lines changed

src/IsaacApiTypes.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,9 +657,9 @@ export interface UserSummaryWithGroupMembershipDTO extends UserSummaryDTO {
657657
export interface IAssignmentLike {
658658
groupId?: number;
659659
id?: number;
660-
creationDate?: Date;
661-
dueDate?: Date;
662-
scheduledStartDate?: Date;
660+
creationDate?: Date | number;
661+
dueDate?: Date | number;
662+
scheduledStartDate?: Date | number;
663663
ownerUserId?: number;
664664
}
665665

src/app/components/elements/Assignments.tsx

Lines changed: 88 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,26 @@ import React, {useMemo, useState} from "react";
33
import {Button, Col, Collapse, Label, Row} from "reactstrap";
44
import {Link} from "react-router-dom";
55
import {
6+
above,
67
determineGameboardStagesAndDifficulties,
78
determineGameboardSubjects,
89
difficultyShortLabelMap,
910
extractTeacherName,
1011
generateGameboardSubjectHexagons,
12+
HUMAN_SUBJECTS,
1113
isDefined,
1214
PATHS,
1315
siteSpecific,
1416
stageLabelMap,
1517
TAG_ID,
16-
tags
18+
tags,
19+
useDeviceSize
1720
} from "../../services";
18-
import {formatDate} from "./DateString";
21+
import {formatDate, FRIENDLY_DATE, getFriendlyDaysUntil} from "./DateString";
1922
import {Circle} from "./svg/Circle";
23+
import { PhyHexIcon } from "./svg/PhyHexIcon";
24+
import { Subject } from "../../../IsaacAppTypes";
25+
import { HoverableTooltip } from "./HoverableTooltip";
2026

2127
const midnightOf = (date: Date | number) => {
2228
const d = new Date(date);
@@ -41,6 +47,8 @@ const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
4147
const now = new Date();
4248
const boardStagesAndDifficulties = useMemo(() => determineGameboardStagesAndDifficulties(assignment.gameboard), [assignment.gameboard]);
4349

50+
const deviceSize = useDeviceSize();
51+
4452
const topics = tags.getTopicTags(Array.from((assignment.gameboard?.contents || []).reduce((a, c) => {
4553
if (isDefined(c.tags) && c.tags.length > 0) {
4654
return new Set([...Array.from(a), ...c.tags.map(id => id as TAG_ID)]);
@@ -49,71 +57,90 @@ const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
4957
}, new Set<TAG_ID>())).filter(tag => isDefined(tag))).map(tag => tag.title).sort();
5058
const assignmentStartDate = assignment.scheduledStartDate ?? assignment.creationDate;
5159

52-
return <>
53-
<hr />
54-
<Row className="board-card" data-testid="my-assignment">
55-
<Col xs={8} sm={10} md={8}>
56-
<Link to={`${PATHS.GAMEBOARD}#${assignment.gameboardId}`}>
57-
<h4 className="text-break">{isDefined(assignment.gameboard) && assignment.gameboard.title}</h4>
58-
</Link>
59-
{isDefined(assignmentStartDate) &&
60-
<p className="mb-0" data-testid={"gameboard-assigned"}><strong>Assigned:</strong> {formatDate(assignmentStartDate)}</p>
61-
}
62-
{isDefined(assignment.dueDate) && isDefined(assignment.gameboard) && now > midnightOf(assignment.dueDate) && assignment.gameboard.percentageAttempted !== 100
63-
? <p className="mb-0"><strong className="overdue">Overdue:</strong> {formatDate(assignment.dueDate)}</p>
64-
: <>{assignment.dueDate && <p className="mb-0"><strong>Due:</strong> {formatDate(assignment.dueDate)}</p>}</>
65-
}
66-
{isDefined(assignment.groupName) &&
67-
<p className="mb-0"><strong>Group:</strong> {assignment.groupName}</p>
68-
}
69-
{isDefined(assignment.assignerSummary) &&
70-
<p className="mb-0"><strong>By:</strong> {extractTeacherName(assignment.assignerSummary)}</p>
71-
}
72-
{isDefined(assignment.notes) && <p className="mb-0"><strong>Notes:</strong> {assignment.notes}</p>}
73-
<Button className="my-2 btn-underline" color="link" onClick={() => setShowMore(!showMore)}>
74-
{showMore ? "Show less" : "Show more"}
75-
</Button>
76-
</Col>
60+
const boardSubjects = determineGameboardSubjects(assignment.gameboard);
7761

78-
<Col xs={4} sm={2} md={4}>
79-
<Row className="justify-content-end me-0 me-md-1">
80-
<Col md="auto">
81-
<Label className="d-block w-100 text-center text-nowrap">
82-
Attempted
83-
<div className="d-flex w-100 justify-content-center board-subject-hexagon-size">
84-
<div className="board-subject-hexagon-container justify-content-center">
85-
{isDefined(assignment.gameboard) && ((assignment.gameboard.percentageAttempted === 100) ?
86-
<span className="board-subject-hexagon subject-complete"/> :
87-
<>
88-
{generateGameboardSubjectHexagons(determineGameboardSubjects(assignment.gameboard))}
89-
<div className="board-percent-completed">{assignment.gameboard.percentageAttempted ?? 0}</div>
90-
</>
91-
)}
92-
</div>
93-
</div>
94-
</Label>
95-
</Col>
96-
<Col md="auto">
97-
<Label className="d-block w-100 text-center text-nowrap">
98-
Correct
99-
<div className="d-flex w-100 justify-content-center board-subject-hexagon-size">
100-
<div className="board-subject-hexagon-container justify-content-center">
101-
{isDefined(assignment.gameboard) && ((assignment.gameboard.percentageCorrect === 100) ?
102-
<span className="board-subject-hexagon subject-complete"/> :
103-
<>
104-
{generateGameboardSubjectHexagons(determineGameboardSubjects(assignment.gameboard))}
105-
<div className="board-percent-completed">{assignment.gameboard.percentageCorrect ?? 0}</div>
106-
</>
107-
)}
108-
</div>
109-
</div>
110-
</Label>
62+
return <Link className="w-100 assignments-card px-3 py-2 mb-3" to={`${PATHS.GAMEBOARD}#${assignment.gameboardId}`}>
63+
<Row data-testid="my-assignment">
64+
<Col xs={8}>
65+
<div className="d-flex align-items-center">
66+
<div className="d-flex justify-content-center board-subject-hexagon-size me-4 my-2">
67+
<div className="board-subject-hexagon-container justify-content-center">
68+
{generateGameboardSubjectHexagons(boardSubjects)}
69+
</div>
70+
<PhyHexIcon icon="page-icon-question-pack" subject={boardSubjects[0] as Subject} className="assignment-hex ps-3"/>
71+
</div>
72+
<div className="d-flex flex-column flex-grow-1">
73+
<h4 className="text-break m-0">{isDefined(assignment.gameboard) && assignment.gameboard.title}</h4>
74+
{above['sm'](deviceSize) && boardSubjects.length > 0 && <div className="d-flex align-items-center mb-2">
75+
{boardSubjects.map((subject) => <span key={subject} className="badge rounded-pill bg-theme me-1" data-bs-theme={subject}>{HUMAN_SUBJECTS[subject]}</span>)}
76+
</div>}
77+
</div>
78+
</div>
79+
80+
<Row>
81+
<Col>
82+
{isDefined(assignmentStartDate) &&
83+
<p className="mb-0" data-testid={"gameboard-assigned"}>
84+
Assigned{" "}
85+
{ getFriendlyDaysUntil(assignmentStartDate).startsWith("on ")
86+
? <strong>{getFriendlyDaysUntil(assignmentStartDate)}</strong>
87+
: <HoverableTooltip tooltip={formatDate(assignmentStartDate, FRIENDLY_DATE)}>
88+
<strong>{getFriendlyDaysUntil(assignmentStartDate)}</strong>
89+
</HoverableTooltip>
90+
}
91+
</p>
92+
}
93+
{isDefined(assignment.dueDate) && isDefined(assignment.gameboard) && now > midnightOf(assignment.dueDate) && assignment.gameboard.percentageAttempted !== 100
94+
? <p className="mb-0"><strong className="overdue">Overdue</strong> <span className="small text-muted">(due {formatDate(assignment.dueDate)})</span></p>
95+
: <>{assignment.dueDate && <p className="mb-0">Due <strong>{getFriendlyDaysUntil(assignment.dueDate)}</strong></p>}</>
96+
}
11197
</Col>
98+
{above['md'](deviceSize) && <Col>
99+
{isDefined(assignment.groupName) &&
100+
<p className="mb-0"><strong>Group:</strong> {assignment.groupName}</p>
101+
}
102+
{isDefined(assignment.assignerSummary) &&
103+
<p className="mb-0"><strong>By:</strong> {extractTeacherName(assignment.assignerSummary)}</p>
104+
}
105+
</Col>}
112106
</Row>
107+
108+
{assignment.notes && <p className="mb-0"><strong>Notes:</strong> {assignment.notes}</p>}
109+
<Button className="my-2 btn-underline" color="link" onClick={(e) => {e.preventDefault(); setShowMore(!showMore);}}>
110+
{showMore ? "Hide details" : "Show details"}
111+
</Button>
112+
</Col>
113+
114+
<Col xs={4}>
115+
<div className="d-flex flex-wrap justify-content-center justify-content-md-end justify-content-lg-center justify-content-xl-end column-gap-4">
116+
<Label className="d-block w-max-content text-center text-nowrap pt-3">
117+
{isDefined(assignment.gameboard) && ((assignment.gameboard.percentageAttempted === 100) ?
118+
<span className="board-subject-hexagon subject-complete"/> :
119+
<div className="board-percent-completed">{assignment.gameboard.percentageAttempted ?? 0}</div>
120+
)}
121+
Attempted
122+
</Label>
123+
<Label className="d-block w-max-content text-center text-nowrap pt-3">
124+
{isDefined(assignment.gameboard) && ((assignment.gameboard.percentageCorrect === 100) ?
125+
<span className="board-subject-hexagon subject-complete"/> :
126+
<div className="board-percent-completed">{assignment.gameboard.percentageCorrect ?? 0}</div>
127+
)}
128+
Correct
129+
</Label>
130+
</div>
113131
</Col>
114132
</Row>
115133
<Collapse isOpen={showMore} className="w-100">
116134
<Row>
135+
{!above['md'](deviceSize) && <Col xs={12}>
136+
{isDefined(assignment.groupName) &&
137+
<p className="mb-0"><strong>Group:</strong> {assignment.groupName}</p>
138+
}
139+
{isDefined(assignment.assignerSummary) &&
140+
<p className="mb-0"><strong>By:</strong> {extractTeacherName(assignment.assignerSummary)}</p>
141+
}
142+
</Col>}
143+
117144
<Col xs={12} md={8} className="mt-sm-2">
118145
<p className="mb-0"><strong>Questions:</strong> {assignment.gameboard?.contents?.length || "0"}</p>
119146
{isDefined(topics) && topics.length > 0 && <p className="mb-0">
@@ -149,7 +176,7 @@ const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
149176
</Col>
150177
</Row>
151178
</Collapse>
152-
</>;
179+
</Link>;
153180
};
154181

155182
const CSAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {

src/app/components/elements/DateString.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export const NUMERIC_DATE_AND_TIME = new Intl.DateTimeFormat(dateLocale, {year:
1010
// Wed, 22 Jan 2020, 13:00
1111
export const FRIENDLY_DATE_AND_TIME = new Intl.DateTimeFormat(dateLocale, {weekday: "short", day: "numeric", month: "short", year: "numeric", hour: "numeric", minute: "2-digit", hour12: false});
1212

13-
export function formatDate(date: number | Date | undefined) {
13+
export function formatDate(date: number | Date | undefined, formatter=NUMERIC_DATE) {
1414
if (!date) return "Unknown";
1515
const dateObject = new Date(date);
16-
return NUMERIC_DATE.format(dateObject);
16+
return formatter.format(dateObject);
1717
}
1818

1919
// 2020-01-22
@@ -31,3 +31,34 @@ export const DateString = ({children, defaultValue, formatter=FRIENDLY_DATE_AND_
3131
return fallback;
3232
}
3333
};
34+
35+
/**
36+
* Calculates and returns a friendly string representing the number of days until a given date.
37+
* If the date is in the future, return "today" if today, "tomorrow" if tomorrow, "in x days" if within a week, "on <date>" if later.
38+
* If the date is in the past, return "yesterday" if yesterday, "x days ago" if within a week, "on <date>" if earlier.
39+
* @param date the date to compare against
40+
* @returns the friendly string.
41+
*/
42+
export function getFriendlyDaysUntil(date: number | Date) : string {
43+
const today = new Date();
44+
45+
const getStartOfDay = (date: number | Date): Date => {
46+
const dateObject = new Date(date);
47+
dateObject.setHours(0, 0, 0, 0);
48+
return dateObject;
49+
};
50+
51+
const daysUntil = (getStartOfDay(date).getTime() - getStartOfDay(today).getTime()) / 86400000;
52+
53+
if (daysUntil < 0) {
54+
// in the past
55+
if (daysUntil === -1) return "yesterday";
56+
if (daysUntil && daysUntil > -7) return `${-daysUntil} days ago`;
57+
} else {
58+
// in the future
59+
if (daysUntil === 0) return "today";
60+
if (daysUntil === 1) return "tomorrow";
61+
if (daysUntil && daysUntil < 7) return `in ${daysUntil} days`;
62+
}
63+
return `on ${FRIENDLY_DATE.format(new Date(date))}`;
64+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { useRef } from "react";
2+
import { UncontrolledTooltip } from "reactstrap";
3+
4+
interface HoverablePopupProps {
5+
tooltip: string;
6+
}
7+
8+
export const HoverableTooltip = ({tooltip, children}: React.PropsWithChildren<HoverablePopupProps>) => {
9+
const ref = useRef<HTMLDivElement>(null);
10+
11+
return <div ref={ref} className="hoverable-tooltip">
12+
<UncontrolledTooltip target={ref} placement="top">
13+
{tooltip}
14+
</UncontrolledTooltip>
15+
{children}
16+
</div>;
17+
};

0 commit comments

Comments
 (0)