Skip to content

Commit 948082f

Browse files
authored
Merge pull request #243 from isaacphysics/cs-related-questions
Cs related questions
2 parents 1f24c31 + 92ba007 commit 948082f

File tree

3 files changed

+100
-24
lines changed

3 files changed

+100
-24
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: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
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";
8-
import {SITE_SUBJECT, SITE} from "../../services/siteConstants";
8+
import {SITE, SITE_SUBJECT} from "../../services/siteConstants";
9+
import {sortByNumberStringValue, sortByStringValue} from "../../services/sorting";
10+
911

1012
interface RelatedContentProps {
1113
content: ContentSummaryDTO[];
1214
parentPage: ContentDTO;
1315
logAction: (eventDetails: object) => void;
1416
}
1517

18+
type RenderItemFunction = (contentSummary: ContentSummaryDTO, openInNewTab?: boolean) => ReactNode;
19+
1620
function getEventDetails(contentSummary: ContentSummaryDTO, parentPage: ContentDTO) {
1721
const eventDetails: any = {};
1822

@@ -48,36 +52,51 @@ function getURLForContent(content: ContentSummaryDTO) {
4852
return `/${documentTypePathPrefix[content.type as DOCUMENT_TYPE]}/${content.id}`
4953
}
5054

51-
export const RelatedContentComponent = ({content, parentPage, logAction}: RelatedContentProps) => {
52-
const concepts = content.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.CONCEPT);
53-
const questions = content.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.QUESTION).sort((a, b) => {
54-
if (a.level === b.level) return ((a.title || '').localeCompare((b.title || ''), undefined, { numeric: true, sensitivity: 'base' }));
55-
const aInt = parseInt(a.level || '-1');
56-
const bInt = parseInt(b.level || '-1');
57-
return aInt > bInt ? 1 : aInt != bInt ? -1 : 0;
58-
});
55+
function renderQuestions(allQuestions: ContentSummaryDTO[], renderItem: RenderItemFunction) {
56+
const evenQuestions = allQuestions.filter((q, i) => i % 2 == 0);
57+
const oddQuestions = allQuestions.filter((q, i) => i % 2 == 1);
5958

60-
const makeListGroupItem = (contentSummary: ContentSummaryDTO) => (
61-
<ListGroupItem key={getURLForContent(contentSummary)} className="w-100 mr-lg-3">
62-
<Link to={getURLForContent(contentSummary)}
63-
onClick={() => {logAction(getEventDetails(contentSummary, parentPage))}}
64-
>
65-
{/*TODO CS Level*/}
66-
{SITE_SUBJECT === SITE.PHY && contentSummary.level && contentSummary.level != '0' ? (contentSummary.title + " (Level " + contentSummary.level + ")") : contentSummary.title}
67-
</Link>
68-
</ListGroupItem>
69-
);
59+
if (allQuestions.length == 0) return null;
60+
return <div className="d-flex align-items-stretch flex-wrap no-print">
61+
<div className="w-100 d-flex">
62+
<div className="flex-fill simple-card my-3 p-3 text-wrap">
63+
<div className="related-questions related-title">
64+
<h5 className="my-2">Related questions</h5>
65+
</div>
66+
<hr/>
67+
{/* Large devices - multi column */}
68+
<div className="d-none d-lg-flex">
69+
<ListGroup className="w-50">
70+
{evenQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
71+
</ListGroup>
72+
<ListGroup className="w-50">
73+
{oddQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
74+
</ListGroup>
75+
</div>
76+
{/* Small devices - single column */}
77+
<div className="d-lg-none">
78+
<ListGroup>
79+
{allQuestions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS))}
80+
</ListGroup>
81+
</div>
82+
</div>
83+
</div>
84+
</div>
85+
}
86+
87+
function renderConceptsAndQuestions(concepts: ContentSummaryDTO[], questions: ContentSummaryDTO[], renderItem: RenderItemFunction) {
88+
if (concepts.length == 0 && questions.length == 0) return null;
7089
return <div className="d-flex align-items-stretch flex-wrap no-print">
7190
<div className="w-100 w-lg-50 d-flex">
7291
<div className="flex-fill simple-card mr-lg-3 my-3 p-3 text-wrap">
7392
<div className="related-concepts related-title">
74-
<h5 className="mb-2">Related concepts</h5>
93+
<h5 className="mb-2">Related Concepts</h5>
7594
</div>
7695
<hr/>
7796
<div className="d-lg-flex">
7897
<ListGroup className="mr-lg-3">
7998
{concepts.length > 0 ?
80-
concepts.map(contentSummary => makeListGroupItem(contentSummary)):
99+
concepts.map(contentSummary => renderItem(contentSummary)):
81100
<div className="mt-2 ml-3">There are no related concepts</div>
82101
}
83102
</ListGroup>
@@ -87,20 +106,52 @@ export const RelatedContentComponent = ({content, parentPage, logAction}: Relate
87106
<div className="w-100 w-lg-50 d-flex">
88107
<div className="flex-fill simple-card ml-lg-3 my-3 p-3 text-wrap">
89108
<div className="related-questions related-title">
90-
<h5 className="mb-2">Related questions</h5>
109+
<h5 className="mb-2">Related Questions</h5>
91110
</div>
92111
<hr/>
93112
<div className="d-lg-flex">
94113
<ListGroup className="mr-lg-3">
95114
{questions.length > 0 ?
96-
questions.map(contentSummary => makeListGroupItem(contentSummary)) :
115+
questions.map(contentSummary => renderItem(contentSummary, SITE_SUBJECT == SITE.CS)) :
97116
<div className="mt-2 ml-3">There are no related questions</div>
98117
}
99118
</ListGroup>
100119
</div>
101120
</div>
102121
</div>
103122
</div>
123+
}
124+
125+
export const RelatedContentComponent = ({content, parentPage, logAction}: RelatedContentProps) => {
126+
// level, difficulty, title; all ascending (reverse the calls for required ordering)
127+
const sortedContent = content
128+
.sort(sortByStringValue("title"))
129+
.sort(sortByNumberStringValue("difficulty"))
130+
.sort(sortByNumberStringValue("level"));
131+
132+
const concepts = sortedContent
133+
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.CONCEPT);
134+
const questions = sortedContent
135+
.filter((contentSummary) => contentSummary.type == DOCUMENT_TYPE.QUESTION);
136+
137+
const makeListGroupItem: RenderItemFunction = (contentSummary: ContentSummaryDTO, openInNewTab?: boolean) => (
138+
<ListGroupItem key={getURLForContent(contentSummary)} className="w-100 mr-lg-3">
139+
<Link
140+
to={getURLForContent(contentSummary)}
141+
onClick={() => {logAction(getEventDetails(contentSummary, parentPage))}}
142+
target={openInNewTab ? "_blank" : undefined}
143+
>
144+
{contentSummary.title}
145+
{/*TODO CS Level*/}
146+
{SITE_SUBJECT === SITE.PHY && contentSummary.level && contentSummary.level != '0' && " (Level " + contentSummary.level + ")"}
147+
</Link>
148+
</ListGroupItem>
149+
);
150+
151+
return {
152+
[SITE.PHY]: renderConceptsAndQuestions(concepts, questions, makeListGroupItem),
153+
[SITE.CS]: renderQuestions(questions, makeListGroupItem)
154+
}[SITE_SUBJECT];
104155
};
105156

106157
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)