Skip to content

Commit 367cd2c

Browse files
committed
Add links to exercises in exercises overview
1 parent 067210d commit 367cd2c

8 files changed

Lines changed: 186 additions & 104 deletions

File tree

plugins/gatsby-transformer-moocfi-exercises/gatsby-node.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const GraphQLString = require('gatsby/graphql').GraphQLString
2-
const GraphQLList = require('gatsby/graphql').GraphQLList
3-
const GraphQLObjectType = require('gatsby/graphql').GraphQLObjectType
1+
const GraphQLString = require("gatsby/graphql").GraphQLString
2+
const GraphQLList = require("gatsby/graphql").GraphQLList
3+
const GraphQLObjectType = require("gatsby/graphql").GraphQLObjectType
44

55
const quiznatorRegex = /<\s*quiznator\s*id\s*=\s*['"]\s*(\w+)\s*['"]\s*>/gm
66
const programmingExerciseTagRegex = /<\s*programming-exercise\s+(.*)\s*>/gm
@@ -32,6 +32,12 @@ const ExerciseType = new GraphQLObjectType({
3232
return details.type
3333
},
3434
},
35+
parentPagePath: {
36+
type: GraphQLString,
37+
resolve(details) {
38+
return details.parentPagePath
39+
},
40+
},
3541
},
3642
})
3743

@@ -43,23 +49,33 @@ exports.setFieldsOnGraphQLNodeType = ({ type }) => {
4349
resolve: (node, _fieldArgs) => {
4450
const source = node.rawMarkdownBody
4551
const quizzes = getMatches(source, quiznatorRegex, 1).map(res => {
46-
return { id: res.match, location: res.location, type: 'quiznator' }
52+
return {
53+
id: res.match,
54+
location: res.location,
55+
type: "quiznator",
56+
parentPagePath: node.frontmatter.path,
57+
}
4758
})
4859
const programmingExercises = getMatches(
4960
source,
5061
programmingExerciseTagRegex,
51-
1
62+
1,
5263
).map(res => {
53-
let id = 'unknown'
64+
let id = "unknown"
5465
try {
5566
const match = getMatches(
5667
res.match,
5768
programmingExerciseNameRegex,
58-
1
69+
1,
5970
)[0].match
6071
id = match.substr(1, match.length - 2)
6172
} catch (e) {}
62-
return { id, location: res.location, type: 'programming-exercise' }
73+
return {
74+
id,
75+
location: res.location,
76+
type: "programming-exercise",
77+
parentPagePath: node.frontmatter.path,
78+
}
6379
})
6480

6581
return programmingExercises.concat(quizzes).sort(function(a, b) {

src/partials/ExercisesInThisSection/ExerciseSummary.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React from "react"
22
import withSimpleErrorBoundary from "../../util/withSimpleErrorBoundary"
33
import styled from "styled-components"
4+
import { normalizeExerciseId } from "../../util/strings"
5+
import { Link } from "gatsby"
46

5-
const ExerciseSummaryWrapper = styled.div`
7+
const ExerciseSummaryWrapper = styled(Link)`
68
padding-left: 1rem;
79
margin-bottom: 0.5rem;
10+
display: block;
811
`
912

1013
const ExerciseSummary = ({ exercise, index, quizIdToTitle }) => {
@@ -20,8 +23,11 @@ const ExerciseSummary = ({ exercise, index, quizIdToTitle }) => {
2023
if (exercise.type === "programming-exercise") {
2124
description = `Ohjelmointitehtävä: ${exercise.id}`
2225
}
26+
let anchorLinkDigest = normalizeExerciseId(`${exercise.type}-${exercise.id}`)
2327
return (
24-
<ExerciseSummaryWrapper>
28+
<ExerciseSummaryWrapper
29+
to={`${exercise.parentPagePath}#${anchorLinkDigest}`}
30+
>
2531
{index + 1}. {description}
2632
</ExerciseSummaryWrapper>
2733
)

src/partials/ProgrammingExercise.js

Lines changed: 106 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import Modal from "@material-ui/core/Modal"
1414
import LoginStateContext from "../contexes/LoginStateContext"
1515
import LoginControls from "../components/LoginControls"
1616
import withSimpleErrorBoundary from "../util/withSimpleErrorBoundary"
17+
import { normalizeExerciseId } from "../util/strings"
18+
import Loading from "../components/Loading"
1719

1820
const accentColor = "#FAAA38"
1921

@@ -174,7 +176,9 @@ class ProgrammingExercise extends React.Component {
174176
}
175177

176178
return (
177-
<ProgrammingExerciseWrapper>
179+
<ProgrammingExerciseWrapper
180+
id={normalizeExerciseId(`programming-exercise-${name}`)}
181+
>
178182
<Header>
179183
<StyledIcon icon={icon} size="1x" />
180184
<HeaderMuted>Tehtävä: </HeaderMuted>
@@ -199,102 +203,111 @@ class ProgrammingExercise extends React.Component {
199203
)}
200204
</div>
201205

202-
{this.context.loggedIn && this.state.exerciseDetails && (
203-
<div>
204-
{tokenThreshHold && (
205-
<Fragment>
206-
<StyledDivider />
207-
<TokenContainer>
208-
<p>
209-
Joka kerta kun olet saanut <i>{tokenThreshHold}</i>:ta
210-
tehtävää tehtyä, saat kolikon. Kolikoilla voi ostaa
211-
tehtävien vastauksia ja lunastaa itsesi mahdollisesta
212-
jumista.{" "}
213-
{availableTokens > 0 ? (
214-
<span>
215-
{" "}
216-
Käytössäsi on tällä hetkellä {availableTokens}{" "}
217-
kolikkoa.
218-
</span>
219-
) : (
220-
<span>Sinulla ei ole vielä yhtään kolikkoa.</span>
206+
{this.context.loggedIn && (
207+
<Loading
208+
loading={this.state.exerciseDetails === undefined}
209+
heightHint="305px"
210+
>
211+
<div>
212+
{tokenThreshHold && (
213+
<Fragment>
214+
<StyledDivider />
215+
<TokenContainer>
216+
<p>
217+
Joka kerta kun olet saanut <i>{tokenThreshHold}</i>:ta
218+
tehtävää tehtyä, saat kolikon. Kolikoilla voi ostaa
219+
tehtävien vastauksia ja lunastaa itsesi mahdollisesta
220+
jumista.{" "}
221+
{availableTokens > 0 ? (
222+
<span>
223+
{" "}
224+
Käytössäsi on tällä hetkellä {availableTokens}{" "}
225+
kolikkoa.
226+
</span>
227+
) : (
228+
<span>Sinulla ei ole vielä yhtään kolikkoa.</span>
229+
)}
230+
</p>
231+
<p>
232+
Pysyt katsomaan mallivastauksen ilman kolikkoja Test My
233+
Code -palvelusta sen jälkeen kun olet saanut tehtävän
234+
oikein.
235+
</p>
236+
237+
{availableTokens > 0 && (
238+
<Button
239+
onClick={this.onShowModelSolution}
240+
variant="outlined"
241+
color="secondary"
242+
>
243+
Katso mallivastaus (kuluttaa tokenin)
244+
</Button>
221245
)}
222-
</p>
223-
<p>
224-
Pysyt katsomaan mallivastauksen ilman kolikkoja Test My
225-
Code -palvelusta sen jälkeen kun olet saanut tehtävän
226-
oikein.
227-
</p>
228246

229-
{availableTokens > 0 && (
230-
<Button
231-
onClick={this.onShowModelSolution}
232-
variant="outlined"
233-
color="secondary"
234-
>
235-
Katso mallivastaus (kuluttaa tokenin)
247+
<Button variant="outlined" onClick={this.onUpdate}>
248+
Päivitä
236249
</Button>
237-
)}
250+
</TokenContainer>
238251

239-
<Button variant="outlined" onClick={this.onUpdate}>
240-
Päivitä
241-
</Button>
242-
</TokenContainer>
243-
244-
<Modal
245-
open={this.state.modelSolutionModalOpen}
246-
onClose={this.onModelSolutionModalClose}
247-
>
248-
{this.state.modelSolution && (
249-
<ModalContent>
250-
<h1>Mallivastaus</h1>
251-
{this.state.modelSolution.solution.files.map(
252-
fileEntry => {
253-
console.log(fileEntry)
254-
return (
255-
<Card>
256-
<CardContent>
257-
<h2>{fileEntry.path}</h2>
258-
<pre class="language-java">
259-
{fileEntry.contents}
260-
</pre>
261-
</CardContent>
262-
</Card>
263-
)
264-
},
265-
)}
266-
</ModalContent>
267-
)}
268-
</Modal>
269-
</Fragment>
270-
)}
271-
<p>
272-
Palauta tehtävä palvelimelle tarkistettavaksi Netbeans
273-
ohjelmointiympäristössä:{" "}
274-
<OutboundLink
275-
href="https://materiaalit.github.io/tmc-asennus/netbeans/"
276-
rel="noopener noreferrer"
277-
target="_blank"
278-
>
279-
ohjeet tehtävien palauttamiseen
280-
</OutboundLink>
281-
.
282-
</p>
283-
<p>
284-
Voit myöhemmin katsoa palautuksiasi Test My Code
285-
palautusympäristössä{" "}
286-
<OutboundLink
287-
href={`https://tmc.mooc.fi/exercises/${
288-
this.state.exerciseDetails.id
289-
}?use_clients=1`}
290-
rel="noopener noreferrer"
291-
target="_blank"
292-
>
293-
täältä
294-
</OutboundLink>
295-
.
296-
</p>
297-
</div>
252+
<Modal
253+
open={this.state.modelSolutionModalOpen}
254+
onClose={this.onModelSolutionModalClose}
255+
>
256+
{this.state.modelSolution && (
257+
<ModalContent>
258+
<h1>Mallivastaus</h1>
259+
{this.state.modelSolution.solution.files.map(
260+
fileEntry => {
261+
console.log(fileEntry)
262+
return (
263+
<Card>
264+
<CardContent>
265+
<h2>{fileEntry.path}</h2>
266+
<pre class="language-java">
267+
{fileEntry.contents}
268+
</pre>
269+
</CardContent>
270+
</Card>
271+
)
272+
},
273+
)}
274+
</ModalContent>
275+
)}
276+
</Modal>
277+
</Fragment>
278+
)}
279+
{this.state.exerciseDetails && (
280+
<Fragment>
281+
<p>
282+
Palauta tehtävä palvelimelle tarkistettavaksi Netbeans
283+
ohjelmointiympäristössä:{" "}
284+
<OutboundLink
285+
href="https://materiaalit.github.io/tmc-asennus/netbeans/"
286+
rel="noopener noreferrer"
287+
target="_blank"
288+
>
289+
ohjeet tehtävien palauttamiseen
290+
</OutboundLink>
291+
.
292+
</p>
293+
<p>
294+
Voit myöhemmin katsoa palautuksiasi Test My Code
295+
palautusympäristössä{" "}
296+
<OutboundLink
297+
href={`https://tmc.mooc.fi/exercises/${
298+
this.state.exerciseDetails.id
299+
}?use_clients=1`}
300+
rel="noopener noreferrer"
301+
target="_blank"
302+
>
303+
täältä
304+
</OutboundLink>
305+
.
306+
</p>
307+
</Fragment>
308+
)}
309+
</div>
310+
</Loading>
298311
)}
299312
</Body>
300313
</ProgrammingExerciseWrapper>

src/partials/Quiznator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from "react"
22
import styled from "styled-components"
33
import withSimpleErrorBoundary from "../util/withSimpleErrorBoundary"
4+
import { normalizeExerciseId } from "../util/strings"
45

56
const QuiznatorWrapper = styled.div`
67
code {
@@ -26,7 +27,7 @@ class Quiznator extends React.Component {
2627
return <div>There should be quiz here but no quiz id is specified.</div>
2728
}
2829
return (
29-
<QuiznatorWrapper>
30+
<QuiznatorWrapper id={normalizeExerciseId(`quiznator-${id}`)}>
3031
<div
3132
id={`unloaded-quiznator-${id}`}
3233
className="quiznator-plugin"

src/templates/CourseContentTemplate.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import { capitalizeFirstLetter } from "../util/strings"
2222
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
2323
import { faArrowCircleUp as icon } from "@fortawesome/free-solid-svg-icons"
2424
import EndOfSubSection from "../components/EndOfSubSection"
25+
import { connectableObservableDescriptor } from "rxjs/internal/observable/ConnectableObservable"
26+
import { tryToScrollToSelector } from "../util/dom"
2527

2628
const StyledIcon = styled(FontAwesomeIcon)`
2729
margin-right: 0.25rem;
@@ -55,6 +57,21 @@ export default class CourseContentTemplate extends React.Component {
5557
if (research === undefined) {
5658
navigate("/missing-info")
5759
}
60+
if (typeof window !== "undefined" && window.location.hash) {
61+
const selector = window.location.hash
62+
setTimeout(() => {
63+
tryToScrollToSelector(selector)
64+
}, 100)
65+
setTimeout(() => {
66+
tryToScrollToSelector(selector)
67+
}, 500)
68+
setTimeout(() => {
69+
tryToScrollToSelector(selector)
70+
}, 1000)
71+
setTimeout(() => {
72+
tryToScrollToSelector(selector)
73+
}, 2000)
74+
}
5875
}
5976

6077
render() {

src/templates/CoursePartOverviewTemplate.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export const pageQuery = graphql`
103103
moocfiExercises {
104104
id
105105
type
106+
parentPagePath
106107
}
107108
}
108109
}

src/util/dom.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export function tryToScrollToSelector(selector) {
2+
if (typeof window === "undefined") {
3+
return
4+
}
5+
try {
6+
const element = document.querySelector(selector)
7+
if (!element) {
8+
console.warn("Could not find the element to scroll to.")
9+
return
10+
}
11+
element.scrollIntoView()
12+
} catch (e) {
13+
console.warn("Could not scroll element into view", e)
14+
}
15+
}

0 commit comments

Comments
 (0)