Skip to content

Commit b58665c

Browse files
committed
fixup! fixup! fixup! fixup! fixup! feat: Syllabus page for learner MFE
1 parent 9ee2ab9 commit b58665c

File tree

4 files changed

+150
-147
lines changed

4 files changed

+150
-147
lines changed

src/components/PrintSyllabus.tsx

Lines changed: 43 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,34 @@
1-
import * as React from "react";
2-
import { useRef } from "react";
1+
import * as React from 'react';
2+
import { useRef } from 'react';
33
import { useIntl } from '@edx/frontend-platform/i18n';
4-
import messages from "../messages";
5-
import {Button} from '@openedx/paragon';
6-
7-
export const PrintSyllabus = ({blockData}) => {
8-
const intl = useIntl()
9-
const iframeRef = useRef(null);
10-
const blocks = blockData?.blocks;
11-
const rootBlock = blockData?.blocks[blockData.root];
12-
const makeList = (items: string[] | null) => {
13-
if (!items) {
14-
return "";
15-
}
16-
const itemsList = items.filter(item => !!item).join("</li><li>");
17-
return itemsList
18-
? "<ul><li>" + items.filter(item => !!item).join("</li><li>") + "</li></ul>"
19-
: "";
20-
};
21-
const syllabusList = makeList(rootBlock.children.map(sectionId =>
22-
"<h1>" + blocks[sectionId].display_name + "</h1>" +
23-
makeList(blocks[sectionId].children.map(subsectionId =>
24-
"<h2>" + blocks[subsectionId].display_name + "</h2>" +
25-
makeList(blocks[subsectionId].children.map(unitId =>
26-
"<h3>" + blocks[unitId].display_name + "</h3>" +
27-
makeList(blocks[unitId].children.flatMap((blockId) => (
28-
blocks[blockId]?.links?.map(link => link.text)
29-
)))
30-
))
31-
))
32-
));
4+
import { Button } from '@openedx/paragon';
5+
import messages from '../messages';
6+
import { BlockResponse } from '../hooks';
337

8+
export const PrintSyllabus = ({ blockData }: { blockData: BlockResponse }) => {
9+
const intl = useIntl();
10+
const iframeRef = useRef(null);
11+
const blocks = blockData?.blocks;
12+
const rootBlock = blockData?.blocks[blockData.root];
13+
const makeList = (items: string[] | null) => {
14+
if (!items) {
15+
return '';
16+
}
17+
const itemsList = items.filter(item => !!item).join('</li><li>');
18+
return itemsList
19+
? `<ul><li>${items.filter(item => !!item).join('</li><li>')}</li></ul>`
20+
: '';
21+
};
22+
const syllabusList = makeList(rootBlock.children.map(sectionId => `<h1>${blocks[sectionId].display_name}</h1>${
23+
makeList(blocks[sectionId].children.map(subsectionId => `<h2>${blocks[subsectionId].display_name}</h2>${
24+
makeList(blocks[subsectionId].children.map(unitId => `<h3>${blocks[unitId].display_name}</h3>${
25+
makeList(blocks[unitId].children.flatMap((blockId) => (
26+
blocks[blockId]?.links?.map(link => link.text)
27+
)))}`))}`))}`));
3428

35-
const srcdoc = `<html>
29+
const srcdoc = `<html>
3630
<head>
37-
<title>${intl.formatMessage(messages.syllabusTitle)}</title><!-- -->
31+
<title>${intl.formatMessage(messages.syllabusTitle)}</title>
3832
<style>
3933
body > ul {
4034
padding: 0.125rem;
@@ -80,11 +74,19 @@ ${syllabusList}
8074
</body>
8175
</html>
8276
`;
83-
return (
84-
<div className="d-flex justify-content-end my-2 flex-column">
85-
<iframe srcDoc={srcdoc} ref={iframeRef} className="d-flex w-100" style={{height:'800px'}}></iframe>
86-
<Button variant="outline-primary"
87-
onClick={() => iframeRef.current.contentWindow.print()}>Print</Button>
88-
</div>
89-
)
90-
}
77+
return (
78+
<div className="d-flex justify-content-end my-2">
79+
<iframe
80+
srcDoc={srcdoc}
81+
ref={iframeRef}
82+
className="d-none"
83+
title={intl.formatMessage(messages.syllabusTitle)}
84+
/>
85+
<Button
86+
variant="outline-primary"
87+
onClick={() => iframeRef.current.contentWindow.print()}
88+
>Print
89+
</Button>
90+
</div>
91+
);
92+
};

src/components/Syllabus.tsx

Lines changed: 97 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -3,112 +3,114 @@ import { Link as LinkIcon } from '@openedx/paragon/icons';
33
import * as React from 'react';
44
import { useEffect, useState } from 'react';
55
import { useParams } from 'react-router-dom';
6-
import { Block, BlockMap, UsageId, useBlockData, usePanels, } from '../hooks';
6+
import {
7+
Block, BlockMap, UsageId, useBlockData, usePanels,
8+
} from '../hooks';
79
import { BlockCollapsible } from './BlockCollapsible';
810
import { HighlightMatch } from './HighlightMatch';
9-
import { PrintSyllabus } from "./PrintSyllabus";
11+
import { PrintSyllabus } from './PrintSyllabus';
1012

1113
const filteredBlocks = (
12-
rootId: UsageId | undefined,
13-
blocks: BlockMap | null,
14-
query: string | null,
14+
rootId: UsageId | undefined,
15+
blocks: BlockMap | null,
16+
query: string | null,
1517
): Set<UsageId> | null => {
16-
if (!query || !blocks || !rootId) {
17-
return null;
18-
}
19-
const matches = new Set<UsageId>();
18+
if (!query || !blocks || !rootId) {
19+
return null;
20+
}
21+
const matches = new Set<UsageId>();
2022

21-
function filterBlocks(blockId: UsageId) {
22-
const block = blocks[blockId];
23-
if (!block) {
24-
return false;
25-
}
26-
let foundMatch = false;
27-
if (block.display_name.toLowerCase().includes(query.toLowerCase())) {
28-
matches.add(blockId);
29-
foundMatch = true;
30-
}
31-
if (block.children) {
32-
const childMatch = block.children.filter(filterBlocks);
33-
if (childMatch.length > 0) {
34-
matches.add(blockId);
35-
foundMatch = true;
36-
}
37-
}
38-
return foundMatch;
23+
function filterBlocks(blockId: UsageId) {
24+
const block = blocks[blockId];
25+
if (!block) {
26+
return false;
27+
}
28+
let foundMatch = false;
29+
if (block.display_name.toLowerCase().includes(query.toLowerCase())) {
30+
matches.add(blockId);
31+
foundMatch = true;
32+
}
33+
if (block.children) {
34+
const childMatch = block.children.filter(filterBlocks);
35+
if (childMatch.length > 0) {
36+
matches.add(blockId);
37+
foundMatch = true;
38+
}
3939
}
40+
return foundMatch;
41+
}
4042

41-
filterBlocks(rootId);
43+
filterBlocks(rootId);
4244

43-
return matches;
45+
return matches;
4446
};
4547
export const Syllabus = () => {
46-
const {courseId} = useParams();
47-
const blockData = useBlockData(courseId);
48-
const blocks = blockData?.blocks;
49-
const rootBlock = blockData?.blocks[blockData.root];
50-
const {
51-
isPanelOpen, toggleAll, togglePanel, allOpen, setOpenPanels,
52-
} = usePanels();
53-
useEffect(() => {
54-
if (blocks) {
55-
setOpenPanels(Object.fromEntries(Object.keys(blocks).map(blockId => [blockId, false])));
56-
}
57-
}, [blocks, setOpenPanels]);
58-
const [query, setQuery] = useState('');
59-
if (!blockData || !blocks || !rootBlock) {
60-
return null;
48+
const { courseId } = useParams();
49+
const blockData = useBlockData(courseId);
50+
const blocks = blockData?.blocks;
51+
const rootBlock = blockData?.blocks[blockData.root];
52+
const {
53+
isPanelOpen, toggleAll, togglePanel, allOpen, setOpenPanels,
54+
} = usePanels();
55+
useEffect(() => {
56+
if (blocks) {
57+
setOpenPanels(Object.fromEntries(Object.keys(blocks).map(blockId => [blockId, false])));
6158
}
62-
const matches = filteredBlocks(blockData?.root, blocks, query);
63-
const iterMatches = (
64-
block: Block,
65-
children: (blockId: UsageId) => React.ReactNode,
66-
border = false,
67-
) => (
68-
block?.children?.map((blockId: UsageId) => (!matches || matches.has(blockId)) && (
69-
<BlockCollapsible
70-
query={query}
71-
block={blockData.blocks[blockId]}
72-
key={blockId}
73-
onToggle={() => togglePanel(blockId)}
74-
isOpen={isPanelOpen(blockId)}
75-
border={border}
76-
>
77-
{children(blockId)}
78-
</BlockCollapsible>
79-
))
80-
);
59+
}, [blocks, setOpenPanels]);
60+
const [query, setQuery] = useState('');
61+
if (!blockData || !blocks || !rootBlock) {
62+
return null;
63+
}
64+
const matches = filteredBlocks(blockData?.root, blocks, query);
65+
const iterMatches = (
66+
block: Block,
67+
children: (blockId: UsageId) => React.ReactNode,
68+
border = false,
69+
) => (
70+
block?.children?.map((blockId: UsageId) => (!matches || matches.has(blockId)) && (
71+
<BlockCollapsible
72+
query={query}
73+
block={blockData.blocks[blockId]}
74+
key={blockId}
75+
onToggle={() => togglePanel(blockId)}
76+
isOpen={isPanelOpen(blockId)}
77+
border={border}
78+
>
79+
{children(blockId)}
80+
</BlockCollapsible>
81+
))
82+
);
8183

82-
return (
83-
<div>
84-
<div className="d-flex justify-content-end mb-2">
85-
<SearchField value={query} onChange={setQuery} onSubmit={setQuery}/>
86-
<Button variant="outline-primary" size="sm" onClick={() => toggleAll()}>
87-
{allOpen
88-
? 'Collapse all'
89-
: 'Expand all'}
90-
</Button>
91-
</div>
92-
{rootBlock && iterMatches(rootBlock, (sectionId: UsageId) => (
93-
iterMatches(blocks[sectionId], (subsectionId: UsageId) => (
94-
iterMatches(blocks[subsectionId], (unitId: UsageId) => (
95-
blocks[unitId].children.flatMap((blockId: UsageId) => (
96-
blocks[blockId]?.links?.map(link => (
97-
<div className="d-flex flex-column" key={blockId + link.href}>
98-
<a
99-
className="d-flex ml-2 align-items-center"
100-
href={link.href}
101-
>
102-
<Icon src={LinkIcon} size="xs" className="mr-2"/>
103-
<HighlightMatch query={query} text={link.text}/>
104-
</a>
105-
</div>
106-
))
107-
)).filter(item => !!item)
108-
))
109-
))
110-
), true)}
111-
{blockData && <PrintSyllabus blockData={blockData}/>}
112-
</div>
113-
);
84+
return (
85+
<div>
86+
<div className="d-flex justify-content-end mb-2">
87+
<SearchField value={query} onChange={setQuery} onSubmit={setQuery} />
88+
<Button variant="outline-primary" size="sm" onClick={() => toggleAll()}>
89+
{allOpen
90+
? 'Collapse all'
91+
: 'Expand all'}
92+
</Button>
93+
</div>
94+
{rootBlock && iterMatches(rootBlock, (sectionId: UsageId) => (
95+
iterMatches(blocks[sectionId], (subsectionId: UsageId) => (
96+
iterMatches(blocks[subsectionId], (unitId: UsageId) => (
97+
blocks[unitId].children.flatMap((blockId: UsageId) => (
98+
blocks[blockId]?.links?.map(link => (
99+
<div className="d-flex flex-column" key={blockId + link.href}>
100+
<a
101+
className="d-flex ml-2 align-items-center"
102+
href={link.href}
103+
>
104+
<Icon src={LinkIcon} size="xs" className="mr-2" />
105+
<HighlightMatch query={query} text={link.text} />
106+
</a>
107+
</div>
108+
))
109+
)).filter(item => !!item)
110+
))
111+
))
112+
), true)}
113+
{blockData && <PrintSyllabus blockData={blockData} />}
114+
</div>
115+
);
114116
};

src/hooks.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ const getCourseBlocksUrl = (courseId: string) => {
6161
url.searchParams.append('student_view_data', 'video,html,problem');
6262
url.searchParams.append('username', username);
6363
return url.toString();
64-
6564
};
6665

6766
export const useBlockData = (courseId?: string) => {

src/messages.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ const messages = defineMessages({
66
defaultMessage: 'Syllabus',
77
description: 'For use in various places, e.g. the tab title.',
88
},
9-
collapseAll: {
10-
id: 'plugins.syllabus.collapseAll',
11-
defaultMessage: 'Collapse all',
12-
description: 'Label for the button that collapses the course syllabus.',
13-
},
14-
expandAll: {
15-
id: 'plugins.syllabus.expandAll',
16-
defaultMessage: 'Expand all',
17-
description: 'Label for the button that expands the course syllabus.',
18-
}
9+
collapseAll: {
10+
id: 'plugins.syllabus.collapseAll',
11+
defaultMessage: 'Collapse all',
12+
description: 'Label for the button that collapses the course syllabus.',
13+
},
14+
expandAll: {
15+
id: 'plugins.syllabus.expandAll',
16+
defaultMessage: 'Expand all',
17+
description: 'Label for the button that expands the course syllabus.',
18+
},
1919
});
2020

2121
export default messages;

0 commit comments

Comments
 (0)