Skip to content

Commit 6713845

Browse files
authored
Merge pull request #1289 from isaacphysics/redesign/common-gameboard-card-component
Common Gameboard card component
2 parents f44ea0f + 3d11a14 commit 6713845

16 files changed

+540
-417
lines changed

src/app/components/elements/Assignments.tsx

Lines changed: 27 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
11
import {AssignmentDTO} from "../../../IsaacApiTypes";
2-
import React, {useMemo, useState} from "react";
3-
import {Button, Col, Collapse, Label, Row} from "reactstrap";
2+
import React, {} from "react";
3+
import {Col, Label, Row} from "reactstrap";
44
import {Link} from "react-router-dom";
55
import {
6-
above,
7-
determineGameboardStagesAndDifficulties,
8-
determineGameboardSubjects,
9-
difficultyShortLabelMap,
106
extractTeacherName,
11-
generateGameboardSubjectHexagons,
12-
HUMAN_SUBJECTS,
137
isDefined,
148
PATHS,
15-
siteSpecific,
16-
stageLabelMap,
17-
TAG_ID,
18-
tags,
19-
useDeviceSize
20-
} from "../../services";
21-
import {formatDate, FRIENDLY_DATE, getFriendlyDaysUntil} from "./DateString";
9+
siteSpecific} from "../../services";
10+
import {formatDate, getFriendlyDaysUntil} from "./DateString";
2211
import {Circle} from "./svg/Circle";
23-
import { PhyHexIcon } from "./svg/PhyHexIcon";
24-
import { Subject } from "../../../IsaacAppTypes";
25-
import { HoverableTooltip } from "./HoverableTooltip";
12+
import { GameboardCard, GameboardLinkLocation } from "./cards/GameboardCard";
2613

2714
const midnightOf = (date: Date | number) => {
2815
const d = new Date(date);
@@ -43,140 +30,34 @@ const CSCircle = ({label, percentage}: {percentage: number | unknown, label: str
4330
};
4431

4532
const PhyAssignmentCard = ({assignment}: {assignment: AssignmentDTO}) => {
46-
const [showMore, setShowMore] = useState(false);
4733
const now = new Date();
48-
const boardStagesAndDifficulties = useMemo(() => determineGameboardStagesAndDifficulties(assignment.gameboard), [assignment.gameboard]);
49-
50-
const deviceSize = useDeviceSize();
51-
52-
const topics = tags.getTopicTags(Array.from((assignment.gameboard?.contents || []).reduce((a, c) => {
53-
if (isDefined(c.tags) && c.tags.length > 0) {
54-
return new Set([...Array.from(a), ...c.tags.map(id => id as TAG_ID)]);
55-
}
56-
return a;
57-
}, new Set<TAG_ID>())).filter(tag => isDefined(tag))).map(tag => tag.title).sort();
5834
const assignmentStartDate = assignment.scheduledStartDate ?? assignment.creationDate;
5935

60-
const boardSubjects = determineGameboardSubjects(assignment.gameboard);
61-
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-
}
97-
</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>}
106-
</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>
36+
return <GameboardCard gameboard={assignment.gameboard} linkLocation={GameboardLinkLocation.Card}>
37+
<Row className="w-100">
38+
<Col xs={12} md={6}>
39+
{isDefined(assignmentStartDate) &&
40+
<p className="mb-0" data-testid={"gameboard-assigned"}>
41+
Assigned <strong>{getFriendlyDaysUntil(assignmentStartDate)}</strong>
42+
</p>
43+
}
44+
{isDefined(assignment.dueDate) && isDefined(assignment.gameboard) && now > midnightOf(assignment.dueDate) && assignment.gameboard.percentageAttempted !== 100
45+
? <p className="mb-0"><strong className="overdue">Overdue</strong> <span className="small text-muted">(due {formatDate(assignment.dueDate)})</span></p>
46+
: <>{assignment.dueDate && <p className="mb-0">Due <strong>{getFriendlyDaysUntil(assignment.dueDate)}</strong></p>}</>
47+
}
11248
</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>
49+
<Col>
50+
{isDefined(assignment.groupName) &&
51+
<p className="mb-0"><strong>Group:</strong> {assignment.groupName}</p>
52+
}
53+
{isDefined(assignment.assignerSummary) &&
54+
<p className="mb-0"><strong>By:</strong> {extractTeacherName(assignment.assignerSummary)}</p>
55+
}
13156
</Col>
13257
</Row>
133-
<Collapse isOpen={showMore} className="w-100">
134-
<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-
144-
<Col xs={12} md={8} className="mt-sm-2">
145-
<p className="mb-0"><strong>Questions:</strong> {assignment.gameboard?.contents?.length || "0"}</p>
146-
{isDefined(topics) && topics.length > 0 && <p className="mb-0">
147-
<strong>{topics.length === 1 ? "Topic" : "Topics"}:</strong>{" "}
148-
{topics.join(", ")}
149-
</p>}
150-
</Col>
151-
<Col xs={12} md={4} className="mt-sm-2">
152-
{boardStagesAndDifficulties.length > 0 && <p className="mb-0">
153-
<table className="w-100">
154-
<thead>
155-
<tr>
156-
<th className="w-50">
157-
{`Stage${boardStagesAndDifficulties.length > 1 ? "s" : ""}:`}
158-
</th>
159-
<th className="w-50">
160-
{`Difficult${boardStagesAndDifficulties.some(([, ds]) => ds.length > 1) ? "ies" : "y"}`}
161-
</th>
162-
</tr>
163-
</thead>
164-
<tbody>
165-
{boardStagesAndDifficulties.map(([stage, difficulties]) => <tr key={stage}>
166-
<td className="w-50 align-baseline">
167-
{stageLabelMap[stage]}:
168-
</td>
169-
<td className="w-50 ps-1">
170-
{difficulties.map((d) => difficultyShortLabelMap[d]).join(", ")}
171-
</td>
172-
</tr>)}
173-
</tbody>
174-
</table>
175-
</p>}
176-
</Col>
177-
</Row>
178-
</Collapse>
179-
</Link>;
58+
59+
{assignment.notes && <p className="mb-0"><strong>Notes:</strong> {assignment.notes}</p>}
60+
</GameboardCard>;
18061
};
18162

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

src/app/components/elements/DateString.ts renamed to src/app/components/elements/DateString.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import React, { ReactNode } from "react";
2+
import { HoverableTooltip } from "./HoverableTooltip";
3+
14
const dateLocale = "en-GB";
25
// 13:00
36
export const TIME_ONLY = new Intl.DateTimeFormat(dateLocale, {hour: "numeric", minute: "2-digit", hour12: false});
@@ -36,10 +39,11 @@ export const DateString = ({children, defaultValue, formatter=FRIENDLY_DATE_AND_
3639
* Calculates and returns a friendly string representing the number of days until a given date.
3740
* 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.
3841
* If the date is in the past, return "yesterday" if yesterday, "x days ago" if within a week, "on <date>" if earlier.
42+
* In all cases besides "on <date>", the string is wrapped in a HoverableTooltip showing the exact date.
3943
* @param date the date to compare against
40-
* @returns the friendly string.
44+
* @returns the friendly string as an element.
4145
*/
42-
export function getFriendlyDaysUntil(date: number | Date) : string {
46+
export function getFriendlyDaysUntil(date: number | Date) : NonNullable<ReactNode> {
4347
const today = new Date();
4448

4549
const getStartOfDay = (date: number | Date): Date => {
@@ -50,15 +54,21 @@ export function getFriendlyDaysUntil(date: number | Date) : string {
5054

5155
const daysUntil = (getStartOfDay(date).getTime() - getStartOfDay(today).getTime()) / 86400000;
5256

57+
let result;
58+
5359
if (daysUntil < 0) {
5460
// in the past
55-
if (daysUntil === -1) return "yesterday";
56-
if (daysUntil && daysUntil > -7) return `${-daysUntil} days ago`;
61+
if (daysUntil === -1) result = "yesterday";
62+
if (daysUntil && daysUntil > -7) result = `${-daysUntil} days ago`;
5763
} else {
5864
// in the future
59-
if (daysUntil === 0) return "today";
60-
if (daysUntil === 1) return "tomorrow";
61-
if (daysUntil && daysUntil < 7) return `in ${daysUntil} days`;
65+
if (daysUntil === 0) result = "today";
66+
if (daysUntil === 1) result = "tomorrow";
67+
if (daysUntil && daysUntil < 7) result = `in ${daysUntil} days`;
6268
}
69+
70+
// if the date has a special result, return it with a tooltip showing the exact date
71+
if (result) return <HoverableTooltip tooltip={formatDate(date, FRIENDLY_DATE)}>{result}</HoverableTooltip>;
72+
6373
return `on ${FRIENDLY_DATE.format(new Date(date))}`;
6474
}

src/app/components/elements/Gameboards.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ const Cards = (props: GameboardsCardsProps) => {
131131
.filter(board => boardCompletionSelection(board, boardCompletion));
132132

133133
return filteredBoards && <>
134-
{<Row className={"row-cols-lg-3 row-cols-md-2 row-cols-1"}>
134+
{<Row className={siteSpecific("row-cols-1", "row-cols-lg-3 row-cols-md-2 row-cols-1")}>
135135
{filteredBoards.map(board => <Col key={board.id}>
136136
<BoardCard
137137
board={board}

src/app/components/elements/ShareLink.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ export const ShareLink = ({linkUrl, reducedWidthLink, gameboardId, clickAwayClos
4242
const linkWidth = isMobile() || reducedWidthLink ? 192 : (shareUrl.length * siteSpecific(9, 6));
4343
const showDuplicateAndEdit = gameboardId && isTutorOrAbove(user);
4444
return <div ref={shareLinkDivRef} className={classNames("share-link-icon", className)}>
45-
<button className={siteSpecific("btn-action", classNames({"outline": outline}))} onClick={() => toggleShareLink()} aria-label={buttonAriaLabel} />
45+
<button className={siteSpecific("btn-action", classNames({"outline": outline}))} onClick={(e) => {e.preventDefault(); toggleShareLink();}} aria-label={buttonAriaLabel} />
4646
<div className={`share-link ${showShareLink ? "d-block" : ""} ${showDuplicateAndEdit ? "double-height" : ""}`} style={{width: linkWidth}}>
47-
<input type="text" readOnly ref={shareLink} value={shareUrl} aria-label="Share URL" />
47+
<input type="text" readOnly ref={shareLink} value={shareUrl} onClick={(e) => e.preventDefault()} aria-label="Share URL" />
4848
{showDuplicateAndEdit && <React.Fragment>
4949
{isPhy && <hr className="text-center mt-4" />}
5050
<a href={`${PATHS.GAMEBOARD_BUILDER}?base=${gameboardId}`} className={isPhy ? "px-1" : ""}>

0 commit comments

Comments
 (0)