Skip to content

Commit 45fa286

Browse files
committed
Display cs related questions only
1 parent 7da7cb8 commit 45fa286

File tree

3 files changed

+96
-29
lines changed

3 files changed

+96
-29
lines changed

src/IsaacApiTypes.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export interface ContentSummaryDTO {
246246
summary?: string;
247247
type?: string;
248248
level?: string;
249+
difficulty?: string;
249250
tags?: string[];
250251
url?: string;
251252
correct?: boolean;

src/app/components/elements/RelatedContent.tsx

Lines changed: 71 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import React from "react";
1+
import React, {ReactNode} from "react";
22
import {ListGroup, ListGroupItem} from "reactstrap";
33
import {ContentDTO, ContentSummaryDTO} from "../../../IsaacApiTypes";
44
import {Link} from "react-router-dom";
55
import {DOCUMENT_TYPE, documentTypePathPrefix} from "../../services/constants";
66
import {connect} from "react-redux";
77
import {logAction} from "../../state/actions";
88
import {SITE, SITE_SUBJECT} from "../../services/siteConstants";
9+
import {sortByNumberStringValue, sortByStringValue} from "../../services/sorting";
910

1011
interface RelatedContentProps {
1112
content: ContentSummaryDTO[];
1213
parentPage: ContentDTO;
1314
logAction: (eventDetails: object) => void;
1415
}
1516

17+
type RenderItemFunction = (contentSummary: ContentSummaryDTO, openInNewTab?: boolean) => ReactNode;
18+
1619
function getEventDetails(contentSummary: ContentSummaryDTO, parentPage: ContentDTO) {
1720
const eventDetails: any = {};
1821

@@ -48,43 +51,51 @@ function getURLForContent(content: ContentSummaryDTO) {
4851
return `/${documentTypePathPrefix[content.type as DOCUMENT_TYPE]}/${content.id}`
4952
}
5053

51-
export const RelatedContentComponent = ({content, parentPage, logAction}: RelatedContentProps) => {
52-
const concepts = content
53-
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.CONCEPT);
54+
function renderQuestions(allQuestions: ContentSummaryDTO[], renderItem: RenderItemFunction) {
55+
const evenQuestions = allQuestions.filter((q, i) => i % 2 == 0);
56+
const oddQuestions = allQuestions.filter((q, i) => i % 2 == 1);
5457

55-
const questions = content
56-
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.QUESTION)
57-
.sort((a, b) => {
58-
if (a.level === b.level) return ((a.title || '').localeCompare((b.title || ''), undefined, { numeric: true, sensitivity: 'base' }));
59-
const aInt = parseInt(a.level || '-1');
60-
const bInt = parseInt(b.level || '-1');
61-
return aInt > bInt ? 1 : aInt != bInt ? -1 : 0;
62-
});
63-
64-
const makeListGroupItem = (contentSummary: ContentSummaryDTO) => (
65-
<ListGroupItem key={getURLForContent(contentSummary)} className="w-100 mr-lg-3">
66-
<Link to={getURLForContent(contentSummary)}
67-
onClick={() => {logAction(getEventDetails(contentSummary, parentPage))}}
68-
>
69-
{contentSummary.title}
70-
{SITE_SUBJECT === SITE.PHY && contentSummary.level && contentSummary.level != '0' &&
71-
" (Level " + contentSummary.level + ")"
72-
}
73-
</Link>
74-
</ListGroupItem>
75-
);
58+
if (allQuestions.length == 0) return null;
59+
return <div className="d-flex align-items-stretch flex-wrap no-print">
60+
<div className="w-100 d-flex">
61+
<div className="flex-fill simple-card my-3 p-3 text-wrap">
62+
<div className="related-questions related-title">
63+
<h5 className="my-2">Related questions</h5>
64+
</div>
65+
<hr/>
66+
{/* Large devices - multi column */}
67+
<div className="d-none d-lg-flex">
68+
<ListGroup className="w-50">
69+
{evenQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
70+
</ListGroup>
71+
<ListGroup className="w-50">
72+
{oddQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
73+
</ListGroup>
74+
</div>
75+
{/* Small devices - single column */}
76+
<div className="d-lg-none">
77+
<ListGroup>
78+
{allQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
79+
</ListGroup>
80+
</div>
81+
</div>
82+
</div>
83+
</div>
84+
}
7685

86+
function renderConceptsAndQuestions(concepts: ContentSummaryDTO[], questions: ContentSummaryDTO[], renderItem: RenderItemFunction) {
87+
if (concepts.length == 0 && questions.length == 0) return null;
7788
return <div className="d-flex align-items-stretch flex-wrap no-print">
7889
<div className="w-100 w-lg-50 d-flex">
7990
<div className="flex-fill simple-card mr-lg-3 my-3 p-3 text-wrap">
8091
<div className="related-concepts related-title">
81-
<h5 className="mb-2">Related concepts</h5>
92+
<h5 className="mb-2">Related Concepts</h5>
8293
</div>
8394
<hr/>
8495
<div className="d-lg-flex">
8596
<ListGroup className="mr-lg-3">
8697
{concepts.length > 0 ?
87-
concepts.map(contentSummary => makeListGroupItem(contentSummary)):
98+
concepts.map(contentSummary => renderItem(contentSummary)):
8899
<div className="mt-2 ml-3">There are no related concepts</div>
89100
}
90101
</ListGroup>
@@ -94,20 +105,51 @@ export const RelatedContentComponent = ({content, parentPage, logAction}: Relate
94105
<div className="w-100 w-lg-50 d-flex">
95106
<div className="flex-fill simple-card ml-lg-3 my-3 p-3 text-wrap">
96107
<div className="related-questions related-title">
97-
<h5 className="mb-2">Related questions</h5>
108+
<h5 className="mb-2">Related Questions</h5>
98109
</div>
99110
<hr/>
100111
<div className="d-lg-flex">
101112
<ListGroup className="mr-lg-3">
102113
{questions.length > 0 ?
103-
questions.map(contentSummary => makeListGroupItem(contentSummary)) :
114+
questions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS)) :
104115
<div className="mt-2 ml-3">There are no related questions</div>
105116
}
106117
</ListGroup>
107118
</div>
108119
</div>
109120
</div>
110121
</div>
122+
}
123+
124+
export const RelatedContentComponent = ({content, parentPage, logAction}: RelatedContentProps) => {
125+
// level, difficulty, title; all ascending (reverse the calls for required ordering)
126+
const sortedContent = content
127+
.sort(sortByStringValue("title"))
128+
.sort(sortByNumberStringValue("difficulty"))
129+
.sort(sortByNumberStringValue("level"));
130+
131+
const concepts = sortedContent
132+
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.CONCEPT);
133+
const questions = sortedContent
134+
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.QUESTION);
135+
136+
const makeListGroupItem: RenderItemFunction = (contentSummary: ContentSummaryDTO, openInNewTab?: boolean) => (
137+
<ListGroupItem key={getURLForContent(contentSummary)} className="w-100 mr-lg-3">
138+
<Link
139+
to={getURLForContent(contentSummary)}
140+
onClick={() => {logAction(getEventDetails(contentSummary, parentPage))}}
141+
target={openInNewTab ? "_blank" : undefined}
142+
>
143+
{contentSummary.title}
144+
{SITE_SUBJECT === SITE.PHY && contentSummary.level && contentSummary.level != '0' && " (Level " + contentSummary.level + ")"}
145+
</Link>
146+
</ListGroupItem>
147+
);
148+
149+
return {
150+
[SITE.PHY]: renderConceptsAndQuestions(concepts, questions, makeListGroupItem),
151+
[SITE.CS]: renderQuestions(questions, makeListGroupItem)
152+
}[SITE_SUBJECT];
111153
};
112154

113155
export const RelatedContent = connect(null, {logAction: logAction})(RelatedContentComponent);

src/app/services/sorting.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,28 @@ export const sortOnPredicateAndReverse = (predicate: string, reverse: boolean) =
1111
if (valueFromObject(a, predicate) < valueFromObject(b, predicate)) {return reverse ? 1 : -1;}
1212
else if (valueFromObject(a, predicate) > valueFromObject(b, predicate)) {return reverse ? -1 : 1;}
1313
else {return 0;}
14+
};
15+
16+
export function sortByNumberStringValue<T>(field: keyof T, undefinedFirst: boolean = false) {
17+
return function comparator(a: T, b: T) {
18+
const aValue: string | undefined = a[field] as any;
19+
const bValue: string | undefined = b[field] as any;
20+
if (aValue === bValue) return 0;
21+
if (aValue === undefined) return undefinedFirst ? -1 : 1;
22+
if (bValue === undefined) return undefinedFirst ? 1 : -1;
23+
const aInt = parseInt(aValue);
24+
const bInt = parseInt(bValue);
25+
return aInt > bInt ? 1 : aInt != bInt ? -1 : 0;
26+
};
27+
}
28+
29+
export function sortByStringValue<T>(field: keyof T, undefinedFirst: boolean = false) {
30+
return function comparator(a: T, b: T) {
31+
const aValue: string | undefined = a[field] as any;
32+
const bValue: string | undefined = b[field] as any;
33+
if (aValue === bValue) return 0;
34+
if (aValue === undefined) return undefinedFirst ? -1 : 1;
35+
if (bValue === undefined) return undefinedFirst ? 1 : -1;
36+
return aValue.localeCompare(bValue, undefined, {numeric: true, sensitivity: 'base'});
37+
};
1438
}

0 commit comments

Comments
 (0)