Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
a29bb93
Feat: new Course db table and api routes for self-enrolled courses
alasdairwilson Jan 8, 2026
1a16c91
refactor: rename Course, Section, and Theme types to MaterialCourse, …
alasdairwilson Jan 9, 2026
e475f44
feat: added /courses page to view all courses
alasdairwilson Jan 10, 2026
f8568c0
course defaults and course detail page
alasdairwilson Jan 20, 2026
04e3263
add coloured tags to courses
alasdairwilson Jan 20, 2026
5f27f08
Sort courses by difficulty
alasdairwilson Jan 20, 2026
6d3c9a6
add languages column to courses table
alasdairwilson Jan 20, 2026
2fd8f37
add languages as a distinct column in frontend cards
alasdairwilson Jan 21, 2026
d174d81
long overdue 404 page
alasdairwilson Jan 21, 2026
1c187ea
add course page for hidden pages
alasdairwilson Jan 21, 2026
b940cbe
separate hidden courses for admins fix sync from trying to overwrite
alasdairwilson Jan 21, 2026
1b7746b
formatting
alasdairwilson Jan 21, 2026
d8813bb
feat: edit course tab
alasdairwilson Jan 22, 2026
dd3e998
feat: update display of course languages
alasdairwilson Jan 23, 2026
6185e50
fix: build errors related tot types
alasdairwilson Jan 23, 2026
020631c
feat: implement unenrol functionality for courses
alasdairwilson Jan 27, 2026
3af86bc
removed race condition in docker compose deploy
alasdairwilson Feb 9, 2026
e03339d
added enrol to course cards on courses/
alasdairwilson Feb 9, 2026
4dc1e6a
moved enrol back to courseid but added view course buitton
alasdairwilson Feb 9, 2026
06f258c
fixed course section links on couresgroup
alasdairwilson Feb 9, 2026
8391066
feat add course button
alasdairwilson Feb 9, 2026
6e90436
course progress improvements
alasdairwilson Feb 10, 2026
a4e6e9d
added protection for no problems on a course
alasdairwilson Feb 10, 2026
fb5c9f9
removed useremail hack from course pages api so non logged users get …
alasdairwilson Feb 12, 2026
07ac022
feat: marking courses as completed,
alasdairwilson Feb 12, 2026
cf36c17
ongoing formatting
alasdairwilson Feb 12, 2026
ec5f8b9
feat: format cpp as C++ in tags
alasdairwilson Feb 12, 2026
82746fd
feat: allowed courses to be imported/exported
alasdairwilson Feb 12, 2026
928c50c
fix: improve add course error handling
alasdairwilson Feb 12, 2026
eaaf222
feat: auto sluggify course name if not provider on POST
alasdairwilson Feb 12, 2026
1e94aa6
feat: add course filters component with search, level, tags, and lang…
alasdairwilson Feb 12, 2026
ec91a9a
formaaaaaat
alasdairwilson Feb 12, 2026
d3cbf71
feat: switch to OR behaviour within tags and languages
alasdairwilson Feb 13, 2026
6d8a50f
fix: build errrors
alasdairwilson Feb 13, 2026
45b2dbd
add breadcrumbs to courses
alasdairwilson Mar 12, 2026
98ad69d
feat: add previous and next navigation for event groups (#459)
alasdairwilson Jan 7, 2026
2762dfe
build(deps): bump undici from 6.22.0 to 6.23.0 (#461)
dependabot[bot] Jan 20, 2026
ee6df5c
fix: request enrolment had been broken (#487)
alasdairwilson Jan 22, 2026
34a79ec
build(deps): bump lodash from 4.17.21 to 4.17.23 (#486)
dependabot[bot] Jan 23, 2026
c858e10
build(deps): bump @isaacs/brace-expansion from 5.0.0 to 5.0.1 (#490)
dependabot[bot] Feb 3, 2026
b9e7648
upgrade to next 16 (#501)
alasdairwilson Mar 16, 2026
36186ac
build(deps): bump undici from 6.23.0 to 6.24.1 (#503)
dependabot[bot] Mar 16, 2026
c845604
build(deps): bump simple-git from 3.30.0 to 3.32.3 (#500)
dependabot[bot] Mar 16, 2026
9d62ae0
build(deps): bump systeminformation from 5.30.0 to 5.31.1 (#498)
dependabot[bot] Mar 16, 2026
f87cf7a
build(deps-dev): bump nokogiri from 1.18.9 to 1.19.1 in /docs (#499)
dependabot[bot] Mar 16, 2026
d3555a2
build(deps): bump dompurify from 3.3.0 to 3.3.2 (#504)
dependabot[bot] Mar 16, 2026
54a692f
build(deps-dev): bump faraday from 2.7.11 to 2.14.1 in /docs (#493)
dependabot[bot] Mar 16, 2026
9d08214
feat: add courses to front page
alasdairwilson Mar 17, 2026
19d5002
moved events tests over to new /events
alasdairwilson Mar 17, 2026
a3a2dcf
set of tests for frontend filter/progress on courses
alasdairwilson Mar 17, 2026
cfb15da
extended course tests
alasdairwilson Mar 17, 2026
fe499b7
format pass on new course components
alasdairwilson Mar 17, 2026
4e1f128
some test fixes
alasdairwilson Mar 17, 2026
f2047db
fix poorly designed tests on course edit/add/api
alasdairwilson Mar 17, 2026
60317a9
fix invalid slug test
alasdairwilson Mar 17, 2026
8e7c967
feat: centralise invalid db connection across the app for check-links…
alasdairwilson Mar 17, 2026
d325d98
Merge branch 'main' into courses-revamp
alasdairwilson Mar 17, 2026
4afd03d
formatting
alasdairwilson Mar 17, 2026
14f402c
tightened course enrolment tests
alasdairwilson Mar 17, 2026
91d576f
remove stale test from landing page
alasdairwilson Mar 17, 2026
227d9f7
add unsplash to link check ignore (*they 307)
alasdairwilson Mar 17, 2026
290d573
tests: add additional course access tests
alasdairwilson Mar 18, 2026
7a08a1a
course export to json test
alasdairwilson Mar 18, 2026
44acda9
feat: added course sync protections, unchanged, new and modified courses
alasdairwilson Mar 18, 2026
fca5478
tests: course sync tests
alasdairwilson Mar 19, 2026
f73c912
fix: update visibility checks in course access and sync tests
alasdairwilson Mar 19, 2026
8c9562d
refactor: imrpove cookie clear between e2e tests
alasdairwilson Mar 19, 2026
58c6898
fix: login command by clearing additional cookies and local storage
alasdairwilson Mar 19, 2026
f10f185
update browser list
alasdairwilson Mar 19, 2026
62e4187
add active course localstorage
alasdairwilson Mar 19, 2026
f1fa25a
feat: add CourseActiveActions component and integrate it into CourseC…
alasdairwilson Mar 19, 2026
9e1f26f
feat: swap seamlessly between courses and events.
alasdairwilson Mar 19, 2026
8318b95
tests: added missing test coverage on event switcher
alasdairwilson Mar 19, 2026
09db07e
feat: add context hints when exploring material that is in courses
alasdairwilson Mar 19, 2026
b8c14b8
feat: CourseView component for sidebar
alasdairwilson Mar 19, 2026
d4c2b4e
ci: fix up the ci
alasdairwilson Mar 19, 2026
d59c6b6
fix component tests around event switchjer
alasdairwilson Mar 19, 2026
b84dfcf
batch progress calls as new api route
alasdairwilson Mar 19, 2026
b8ae14d
linting pass
alasdairwilson Mar 19, 2026
50911a9
styling and wording fixes for ispartofcourse hint
alasdairwilson Mar 20, 2026
5f548ef
feat: extended LinkedSection component to work with Course structures
alasdairwilson Mar 20, 2026
61c0397
add course action button to front page course element
alasdairwilson Mar 20, 2026
139bbe5
change LinkedSections to use shared rendertags
alasdairwilson Mar 20, 2026
893ee3d
changed sidebar section links to use the shared courseSectionLink com…
alasdairwilson Mar 20, 2026
49bedd3
fix ally roles
alasdairwilson Mar 23, 2026
7e95114
feat: update UI components for event editing to the single page view
alasdairwilson Mar 23, 2026
506178b
test: eventgroup editor tests
alasdairwilson Mar 23, 2026
68d32b7
misc. test. fixes
alasdairwilson Mar 23, 2026
c51ec25
format
alasdairwilson Mar 23, 2026
1219a03
further linkedsection tests
alasdairwilson Mar 23, 2026
d225fb3
feat: make event from course blueprint <3
alasdairwilson Mar 23, 2026
2ab9c98
changed course material hint messages
alasdairwilson Mar 23, 2026
3fb6130
removed glob pin/upgraded cypress
alasdairwilson Mar 23, 2026
b0338ed
replaced seed data and course defaults with more realistic courses
alasdairwilson Mar 24, 2026
97c6ded
imrpoves the appearance of teh differencing when changing course mate…
alasdairwilson Mar 24, 2026
7800ba3
split course sync diff into separate coursegroups
alasdairwilson Mar 24, 2026
0760401
docs for gutenberg 2.0
alasdairwilson Mar 24, 2026
f2a8dcd
mega docs touchup
alasdairwilson Mar 25, 2026
d1f05a2
feat: added stats pages for courses/events
alasdairwilson Mar 25, 2026
db2f50c
added stats feature to changelog/whatsnew
alasdairwilson Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ jobs:
--exclude "docs.python.org" \
--exclude "mpich.org" \
--exclude "medium.com" \
--exclude "unsplash.com" \
; true)
echo "$OUTPUT"

Expand Down
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ This means you need to copy some source material before you have anything to dis
The HPC materials are already listed as a source under `material` in the configuration yaml file in the `config` directory.

To download those materials, so we have something to work with, run `yarn pullmat` and they will be cloned into
a local `.materials` directory.
a local `.material` directory.

### 5. (Setting up a postgres database)

Expand Down Expand Up @@ -92,6 +92,18 @@ The instructions for doing so are [on the Docker support pages](https://docs.doc
With the database available, we then need to create the various tables etc. that Gutenberg uses with
`yarn prisma migrate dev`.

If you also want the local demo and test seed data, run:

```shell
npx prisma db seed
```

If you want to recreate the database from scratch and reseed it in one step, run:

```shell
yarn prisma migrate reset --force
```

### 6. Start the development server

The local development server can be started with `yarn dev`.
Expand Down
29 changes: 22 additions & 7 deletions components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Course, Section, Theme } from "lib/material"
import { MaterialCourse, MaterialSection, MaterialTheme } from "lib/material"
import Link from "next/link"

import { useSession } from "next-auth/react"
Expand All @@ -11,24 +11,32 @@ import Overlay from "./Overlay"
import Navbar from "./navbar/Navbar"
import { useSidebarOpen } from "lib/hooks/useSidebarOpen"
import useActiveEvent from "lib/hooks/useActiveEvents"
import useActiveCourse from "lib/hooks/useActiveCourse"
import { Provider } from "jotai"
import { PageTemplate } from "lib/pageTemplate"
import { SectionLink } from "./ui/LinkedSection"
import { findLinks } from "lib/findSectionLinks"
import PlausibleProvider from "next-plausible"
import { useTheme } from "next-themes"
import { ThemeProvider } from "@mui/material/styles"
import useSWR, { Fetcher } from "swr"
import { LightTheme, DarkTheme } from "./MuiTheme"
import plausibleHost from "lib/plausibleHost"
import { BreadcrumbItem } from "lib/breadcrumbs"
import { basePath } from "lib/basePath"
import type { Data as ActiveCourseData } from "pages/api/course/byExternal/[externalId]"

const activeCourseFetcher: Fetcher<ActiveCourseData, string> = (url) => fetch(url).then((r) => r.json())

type Props = {
material: Material
theme?: Theme
course?: Course
section?: Section
theme?: MaterialTheme
course?: MaterialCourse
section?: MaterialSection
children: ReactNode
pageInfo: PageTemplate
pageTitle?: string
breadcrumbs?: BreadcrumbItem[]
repoUrl?: string
excludes?: Excludes
}
Expand All @@ -41,15 +49,21 @@ const Layout: React.FC<Props> = ({
children,
pageInfo,
pageTitle,
breadcrumbs,
repoUrl,
excludes,
}) => {
const [activeEvent, setActiveEvent] = useActiveEvent()
const { data: session } = useSession()
const [activeEvent] = useActiveEvent()
const [activeCourseExternalId] = useActiveCourse()
useSession()
const { theme: currentTheme } = useTheme()
const [showAttribution, setShowAttribution] = useState(false)
const { data: activeCourseData } = useSWR(
activeCourseExternalId ? `${basePath}/api/course/byExternal/${activeCourseExternalId}` : undefined,
activeCourseFetcher
)
const [sidebarOpen, setSidebarOpen] = useSidebarOpen(true)
const sectionLinks: SectionLink[] = findLinks(material, theme, course, section, activeEvent)
const sectionLinks: SectionLink[] = findLinks(material, theme, course, section, activeEvent, activeCourseData?.course)

const muiTheme = React.useMemo(() => (currentTheme === "light" ? LightTheme : DarkTheme), [currentTheme])
return (
Expand All @@ -74,6 +88,7 @@ const Layout: React.FC<Props> = ({
repoUrl={repoUrl}
excludes={excludes}
pageInfo={pageInfo}
breadcrumbs={breadcrumbs}
/>
</header>
<main id="main">
Expand Down
8 changes: 4 additions & 4 deletions components/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Course, Material, Section, Theme } from "lib/material"
import { MaterialCourse, Material, MaterialSection, MaterialTheme } from "lib/material"
import { EventFull } from "lib/types"
import { NextPage } from "next"
import React, { useEffect, useState } from "react"
Expand All @@ -19,9 +19,9 @@ import type { PageTemplate } from "lib/pageTemplate"

interface Props {
material: Material
theme?: Theme
course?: Course
section?: Section
theme?: MaterialTheme
course?: MaterialCourse
section?: MaterialSection
activeEvent: EventFull | undefined
showAttribution: boolean
setShowAttribution: (show: boolean) => void
Expand Down
51 changes: 51 additions & 0 deletions components/content/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from "react"
import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter"
import CopyToClipboard from "components/ui/CopyToClipboard"
import { FaClipboard } from "react-icons/fa"
import { lucario as codeStyle } from "react-syntax-highlighter/dist/cjs/styles/prism"

const SyntaxHighlighter = Prism as any as React.FC<SyntaxHighlighterProps>

type Props = {
code: string
language?: string
className?: string
}

const CodeBlock: React.FC<Props> = ({ code, language, className }) => {
if (language) {
return (
<div className="relative m-0">
<CopyToClipboard text={code}>
<button className="group absolute top-0 right-0 bg-transparent text-xs text-grey-700 hover:bg-grey-900 px-2 py-1 rounded flex items-center space-x-1">
<FaClipboard className="group-hover:text-white" />
<span className="group-hover:text-white">Copy</span>
</button>
</CopyToClipboard>
<SyntaxHighlighter
language={language}
PreTag="div"
style={codeStyle}
codeTagProps={{ className: "text-sm" }}
customStyle={{ margin: 0 }}
>
{code}
</SyntaxHighlighter>
</div>
)
}

return (
<div className="p-3 relative">
<CopyToClipboard text={code}>
<button className="group absolute top-0 right-0 bg-transparent text-xs text-grey-700 hover:text-grey-900 px-2 py-1 rounded flex items-center space-x-1">
<FaClipboard className="group-hover:text-white" />
<span className="group-hover:text-white">Copy</span>
</button>
</CopyToClipboard>
<code className={className}>{code}</code>
</div>
)
}

export default CodeBlock
53 changes: 6 additions & 47 deletions components/content/Content.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import React, { JSX, ReactNode, useRef } from "react"
import ReactMarkdown, { ExtraProps, Components } from "react-markdown"
import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter"
import CopyToClipboard from "components/ui/CopyToClipboard"
import { FaClipboard } from "react-icons/fa"

import { lucario as codeStyle } from "react-syntax-highlighter/dist/cjs/styles/prism"
import CodeBlock from "components/content/CodeBlock"

import directive from "remark-directive"
import { visit } from "unist-util-visit"
Expand All @@ -17,14 +13,12 @@ import "katex/dist/katex.min.css" // `rehype-katex` does not import the CSS for
import Challenge from "./Challenge"
import Solution from "./Solution"
import Callout from "./Callout"
import { Course, Section, Theme } from "lib/material"
import { MaterialCourse, MaterialSection, MaterialTheme } from "lib/material"
import Paragraph from "./Paragraph"
import Heading from "./Heading"
import { reduceRepeatingPatterns, extractTextValues } from "lib/utils"
import Link from "next/link"

const SyntaxHighlighter = Prism as any as React.FC<SyntaxHighlighterProps>

type ReactMarkdownProps = React.JSX.IntrinsicElements["p"] & ExtraProps
type HeadingProps = React.JSX.IntrinsicElements["h2"] & ExtraProps
type CodeProps = React.JSX.IntrinsicElements["code"] & ExtraProps
Expand Down Expand Up @@ -111,42 +105,7 @@ function code({ node, className, children, ...props }: CodeProps): React.JSX.Ele
const isBlockCode = Boolean(className && /language-(\w+)/.test(className))

if (isBlockCode) {
if (match) {
return (
<div className="relative m-0">
<CopyToClipboard text={code}>
<button className="group absolute top-0 right-0 bg-transparent text-xs text-grey-700 hover:bg-grey-900 px-2 py-1 rounded flex items-center space-x-1">
<FaClipboard className="group-hover:text-white" />
<span className="group-hover:text-white">Copy</span>
</button>
</CopyToClipboard>
<SyntaxHighlighter
{...(props as any)}
language={match[1]}
PreTag="div"
style={codeStyle}
codeTagProps={{ className: "text-sm" }}
customStyle={{ margin: 0 }}
>
{code}
</SyntaxHighlighter>
</div>
)
} else {
return (
<div className="p-3 relative">
<CopyToClipboard text={code}>
<button className="group absolute top-0 right-0 bg-transparent text-xs text-grey-700 hover:text-grey-900 px-2 py-1 rounded flex items-center space-x-1">
<FaClipboard className="group-hover:text-white" />
<span className="group-hover:text-white">Copy</span>
</button>
</CopyToClipboard>
<code className={className} {...props}>
{children}
</code>
</div>
)
}
return <CodeBlock code={code} language={match?.[1]} className={className} />
} else {
return (
<code className={className} {...props}>
Expand All @@ -158,9 +117,9 @@ function code({ node, className, children, ...props }: CodeProps): React.JSX.Ele

type Props = {
markdown: string
theme?: Theme
course?: Course
section?: Section
theme?: MaterialTheme
course?: MaterialCourse
section?: MaterialSection
}

const Content: React.FC<Props> = ({ markdown, theme, course, section }) => {
Expand Down
26 changes: 20 additions & 6 deletions components/content/LearningOutcomes.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Section } from "../../lib/material"
import { MaterialSection } from "../../lib/material"
import ListItem from "@mui/material/ListItem"
import ListItemText from "@mui/material/ListItemText"
import List from "@mui/material/List"
Expand All @@ -11,17 +11,31 @@ import ListItemIcon from "@mui/material/ListItemIcon"
import React from "react"
import { type Theme } from "@mui/system"

export default function LearningOutcomes({ learningOutcomes }: { learningOutcomes: Section["learningOutcomes"] }) {
export default function LearningOutcomes({
learningOutcomes,
}: {
learningOutcomes: MaterialSection["learningOutcomes"]
}) {
const [open, setOpen] = React.useState(true)
if (learningOutcomes.length === 0) return null
return (
<Alert
severity="success"
className="max-w-2xl mx-auto dark:bg-indigo-950 dark:text-teal-50"
sx={{ marginBottom: (t: Theme) => t.spacing(1) }}
className="mx-auto max-w-2xl border border-emerald-200 !bg-emerald-50 !text-emerald-950 shadow-sm dark:border-emerald-900 dark:!bg-emerald-950/20 dark:!text-emerald-50"
sx={{
marginBottom: (t: Theme) => t.spacing(1),
"& .MuiAlert-message": { width: "100%" },
"& .MuiListItemIcon-root": { minWidth: 36 },
"& .MuiListItemText-primary": { color: "inherit" },
}}
>
<Tooltip title={`Click to ${open ? "hide" : "show"} learning outcomes`}>
<Typography variant="body2" onClick={() => setOpen(!open)} sx={{ cursor: "pointer" }}>
<Typography
variant="body2"
onClick={() => setOpen(!open)}
sx={{ cursor: "pointer", fontWeight: 600 }}
className="text-emerald-900 dark:text-emerald-100"
>
Learning outcomes
</Typography>
</Tooltip>
Expand All @@ -30,7 +44,7 @@ export default function LearningOutcomes({ learningOutcomes }: { learningOutcome
{learningOutcomes.map((o, i) => (
<ListItem key={i}>
<ListItemIcon>
<HiOutlineTrophy color="#2e7d32" />
<HiOutlineTrophy className="text-emerald-600 dark:text-emerald-300" />
</ListItemIcon>
<ListItemText>{o}</ListItemText>
</ListItem>
Expand Down
8 changes: 8 additions & 0 deletions components/content/TableOfContents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,15 @@ const TableOfContents: FC<TableOfContentsProps> = ({ markdown, tocTitle }: Table
<div
className="flex items-center mb-4 absolute top-0 z-50 p-1 bg-transparent"
style={{ cursor: "pointer", pointerEvents: "auto" }}
role="button"
tabIndex={0}
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
window.scrollTo({ top: 0, behavior: "smooth" })
}
}}
>
{collapsed ? (
<button
Expand Down
53 changes: 53 additions & 0 deletions components/courses/CourseActiveActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from "react"
import { Button } from "flowbite-react"
import { CourseStatus } from "@prisma/client"
import { HiArrowNarrowRight } from "react-icons/hi"
import useActiveCourse from "lib/hooks/useActiveCourse"
import useLearningContext from "lib/hooks/useLearningContext"

type Props = {
courseExternalId: string
status: CourseStatus | null
verbose?: boolean
size?: "xs" | "sm"
}

const CourseActiveActions: React.FC<Props> = ({ courseExternalId, status, verbose = false, size = "xs" }) => {
const [activeCourseExternalId, setActiveCourseExternalId] = useActiveCourse()
const [, setLearningContext] = useLearningContext()

const isSelectable = status === CourseStatus.ENROLLED || status === CourseStatus.COMPLETED
const isActiveCourse = activeCourseExternalId === courseExternalId

if (!isSelectable) return null

return isActiveCourse ? (
<Button
size={size}
color="gray"
onClick={() => {
setActiveCourseExternalId(undefined)
setLearningContext(undefined)
}}
style={{ zIndex: 1 }}
>
{verbose ? "Deselect as active course" : "Unselect"}
<HiArrowNarrowRight className="ml-2 h-3 w-3" />
</Button>
) : (
<Button
size={size}
color="gray"
onClick={() => {
setActiveCourseExternalId(courseExternalId)
setLearningContext({ type: "course", externalId: courseExternalId })
}}
style={{ zIndex: 1 }}
>
{verbose ? "Select as your active course" : "Select"}
<HiArrowNarrowRight className="ml-2 h-3 w-3" />
</Button>
)
}

export default CourseActiveActions
Loading