Skip to content

Commit c0fa067

Browse files
committed
First working draft of hash-based navigation for challenge parts
1 parent d32f67c commit c0fa067

File tree

5 files changed

+56
-61
lines changed

5 files changed

+56
-61
lines changed

src/components/challenges/PartsTimeline.js

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
1-
import React, { memo, useState } from 'react';
1+
import React, { memo } from 'react';
22
import cn from 'classnames';
33

44
import * as css from './PartsTimeline.module.css';
55
import { Link } from 'gatsby';
66

7-
const PartsTimeline = ({ className, parts, onPartChange }) => {
8-
const [currentPartIndex, setCurrentPartIndex] = useState(0);
9-
10-
const updatePartIndex = (index) => {
11-
onPartChange(parts[index]);
12-
setCurrentPartIndex(index);
13-
};
14-
7+
const PartsTimeline = ({ className, parts, currentPartIndex }) => {
158
return (
169
<div className={cn(css.root, className)}>
1710
<div className={css.partsTimeline}>
@@ -23,38 +16,19 @@ const PartsTimeline = ({ className, parts, onPartChange }) => {
2316
[css.seen]: index <= currentPartIndex,
2417
[css.last]: index === currentPartIndex
2518
})}>
26-
<Link
27-
to="#"
28-
onClick={(event) => {
29-
event.preventDefault();
30-
updatePartIndex(index);
31-
}}>
32-
{part.title}
33-
</Link>
19+
<Link to={`#part-${index + 1}`}>{part.title}</Link>
3420
</li>
3521
))}
3622
</ul>
3723
</div>
3824
<div className={css.navigation}>
3925
{currentPartIndex > 0 && (
40-
<Link
41-
to="#"
42-
className={css.navButton}
43-
onClick={(event) => {
44-
event.preventDefault();
45-
updatePartIndex(currentPartIndex - 1);
46-
}}>
26+
<Link to={`#part-${currentPartIndex}`} className={css.navButton}>
4727
Previous
4828
</Link>
4929
)}
5030
{currentPartIndex < parts.length - 1 && (
51-
<Link
52-
to="#"
53-
className={css.navButton}
54-
onClick={(event) => {
55-
event.preventDefault();
56-
updatePartIndex(currentPartIndex + 1);
57-
}}>
31+
<Link to={`#part-${currentPartIndex + 2}`} className={css.navButton}>
5832
Next
5933
</Link>
6034
)}

src/components/challenges/VideoSection.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PartsTimeline from './PartsTimeline';
99
import NebulaVideoRow from '../NebulaVideoRow';
1010

1111
import { filteredPath } from '../../utils';
12+
import { useChallengePartIndex } from '../../hooks';
1213

1314
import * as css from './VideoSection.module.css';
1415

@@ -23,9 +24,8 @@ const VideoSection = ({ challenge }) => {
2324
const hasMultiParts = challenge.parts?.length > 0;
2425
const [showTimestamps, setShowTimestamps] = useState(!hasMultiParts);
2526

26-
const [activePart, setActivePart] = useState(
27-
challenge.parts?.[0] ?? challenge
28-
);
27+
const activePartIndex = useChallengePartIndex(challenge.parts?.length ?? 1);
28+
const activePart = challenge.parts?.[activePartIndex] ?? challenge;
2929
const { videoId, nebulaSlug, timestamps } = activePart;
3030
const hasTimestamps = timestamps?.length > 0;
3131
const hasTimeline = hasMultiParts || hasTimestamps;
@@ -147,10 +147,7 @@ const VideoSection = ({ challenge }) => {
147147
[css.hide]: showTimestamps
148148
})}
149149
parts={challenge.parts}
150-
onPartChange={(part) => {
151-
setActivePart(part);
152-
setShowTimeline(false);
153-
}}
150+
currentPartIndex={activePartIndex}
154151
/>
155152
)}
156153
{hasTimestamps && (

src/components/tracks/OverviewTimeline.js

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,43 @@ const usePaths = (chapters, track, trackPosition) => {
1111
.flatMap((chapter) => chapter.videos)
1212
.flatMap((video) =>
1313
video.parts?.length > 0
14-
? video.parts.map((_, partIndex) => ({ slug: video.slug, partIndex }))
15-
: [{ slug: video.slug, partIndex: 0 }]
14+
? video.parts.map((_, partIndex) => ({
15+
slug: video.slug,
16+
partIndex,
17+
isMultiPart: true
18+
}))
19+
: [{ slug: video.slug, partIndex: 0, isMultiPart: false }]
1620
);
17-
const partIndex = useChallengePartIndex();
1821
const currentVideo =
1922
chapters[trackPosition.chapterIndex].videos[trackPosition.videoIndex];
23+
const totalParts = flatTrack.filter(
24+
(video) => video.slug === currentVideo.slug
25+
).length;
26+
const partIndex = useChallengePartIndex(totalParts);
2027
const currentIndex = flatTrack.findIndex(
2128
(video) => video.slug === currentVideo.slug && video.partIndex === partIndex
2229
);
2330
const prevVideo = flatTrack[currentIndex - 1];
2431
const nextVideo = flatTrack[currentIndex + 1];
25-
const computePath = (video) =>
26-
video ? { ...video, path: `/tracks/${track.slug}/${video.slug}` } : null;
27-
return [computePath(prevVideo), computePath(nextVideo)];
32+
const computePath = (video) => {
33+
if (video) {
34+
const hash = video.isMultiPart ? `#part-${video.partIndex + 1}` : '';
35+
return {
36+
...video,
37+
path: `/tracks/${track.slug}/${video.slug}${hash}`
38+
};
39+
}
40+
return null;
41+
};
42+
return [computePath(prevVideo), computePath(nextVideo), partIndex];
2843
};
2944

3045
const OverviewTimeline = ({ className, chapters, track, trackPosition }) => {
31-
const [previousVideo, nextVideo] = usePaths(chapters, track, trackPosition);
46+
const [previousVideo, nextVideo, currentPartIndex] = usePaths(
47+
chapters,
48+
track,
49+
trackPosition
50+
);
3251

3352
const timelineRef = usePersistScrollPosition(track.slug, 'tracks');
3453
return (
@@ -42,23 +61,18 @@ const OverviewTimeline = ({ className, chapters, track, trackPosition }) => {
4261
chapters={chapters}
4362
track={track}
4463
trackPosition={trackPosition}
64+
currentPartIndex={currentPartIndex}
4565
/>
4666
))}
4767
</div>
4868
<div className={css.navigation}>
4969
{previousVideo !== null && (
50-
<Link
51-
className={css.navButton}
52-
to={previousVideo.path}
53-
state={{ challengePartIndex: previousVideo.partIndex }}>
70+
<Link className={css.navButton} to={previousVideo.path}>
5471
Previous
5572
</Link>
5673
)}
5774
{nextVideo !== null && (
58-
<Link
59-
className={css.navButton}
60-
to={nextVideo.path}
61-
state={{ challengePartIndex: nextVideo.partIndex }}>
75+
<Link className={css.navButton} to={nextVideo.path}>
6276
Next
6377
</Link>
6478
)}
@@ -68,14 +82,20 @@ const OverviewTimeline = ({ className, chapters, track, trackPosition }) => {
6882
};
6983

7084
const ChapterSection = memo(
71-
({ chapter, chapterIndex, chapters, track, trackPosition }) => {
85+
({
86+
chapter,
87+
chapterIndex,
88+
chapters,
89+
track,
90+
trackPosition,
91+
currentPartIndex
92+
}) => {
7293
const hasSeenChapter = chapterIndex < trackPosition.chapterIndex;
7394
const isThisChapter = chapterIndex === trackPosition.chapterIndex;
7495
const trackPath = `/tracks/${track.slug}`;
7596
const [collapsed, setCollapsed] = useState(false);
7697

7798
const { videoIndex: currentVideoIndex } = trackPosition;
78-
const currentPartIndex = useChallengePartIndex();
7999

80100
return (
81101
<ul className={css.chapterList}>
@@ -117,8 +137,7 @@ const ChapterSection = memo(
117137
[css.last]: isLastVideo && partIndex === currentPartIndex
118138
})}>
119139
<Link
120-
to={`${trackPath}/${video.slug}`}
121-
state={{ challengePartIndex: partIndex }}>
140+
to={`${trackPath}/${video.slug}#part-${partIndex + 1}`}>
122141
{video.title} - {part.title}
123142
</Link>
124143
</li>

src/components/tracks/VideoSection.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ const VideoSection = ({ track, video, trackPosition, mainTitle }) => {
3535

3636
const { title, topics, languages } = video;
3737

38-
const partIndex = useChallengePartIndex();
38+
const totalParts = video.parts?.length ?? 1;
39+
const partIndex = useChallengePartIndex(totalParts);
3940
const part = video.parts?.[partIndex];
4041
const videoId = part?.videoId ?? video.videoId;
4142
const nebulaSlug = part?.nebulaSlug ?? video.nebulaSlug;

src/hooks/index.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,13 @@ export const useIsFirstRender = () => {
143143
* which has been stored in the `location.state` object using the `Link.state`
144144
* property.
145145
*
146+
* @param totalParts {number} total number of parts of the challenge (1 if the
147+
* challenge is not multi-part)
148+
*
146149
* @returns {number} challenge part index
147150
*/
148-
export const useChallengePartIndex = () => {
149-
const { state } = useLocation();
150-
return state?.challengePartIndex ?? 0;
151+
export const useChallengePartIndex = (totalParts) => {
152+
const { hash } = useLocation();
153+
const [match, partNumberStr] = hash.match(/#part-([1-9][0-9]*)/) || [false];
154+
return match ? Math.min(parseInt(partNumberStr), totalParts) - 1 : 0;
151155
};

0 commit comments

Comments
 (0)