diff --git a/package.json b/package.json index 44373897b..65b09d79a 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "react-fundamentals", "version": "1.0.0", "description": "The material for learning React fundamentals", + "title": "React Fundamentals ⚛", "keywords": [], "homepage": "http://react-fundamentals.netlify.com/", "license": "GPL-3.0-only", @@ -13,22 +14,27 @@ }, "dependencies": { "@reach/router": "^1.2.1", - "@testing-library/react": "^8.0.1", - "history": "^4.9.0", - "jest-dom": "^3.4.0", - "react": "^16.8.6", - "react-dom": "^16.8.6", - "stop-runaway-react-effects": "^1.2.0" + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.4.0", + "chalk": "^3.0.0", + "glob": "^7.1.6", + "history": "^4.10.1", + "normalize.css": "^8.0.1", + "preval.macro": "^4.0.0", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-router-dom": "^5.1.2", + "stop-runaway-react-effects": "^1.2.1" }, "devDependencies": { - "cross-spawn": "^6.0.5", - "husky": "^2.4.0", - "inquirer": "^6.3.1", + "cross-spawn": "^7.0.1", + "husky": "^4.0.10", + "inquirer": "^7.0.3", "is-ci": "^2.0.0", - "npm-run-all": "^4.1.3", - "prettier": "^1.17.1", - "react-scripts": "^3.0.1", - "replace-in-file": "^4.1.0" + "npm-run-all": "^4.1.5", + "prettier": "^1.19.1", + "react-scripts": "^3.3.0", + "replace-in-file": "^5.0.2" }, "scripts": { "start": "react-scripts start", diff --git a/public/index.html b/public/index.html index 1edd238ef..d037de6a2 100644 --- a/public/index.html +++ b/public/index.html @@ -7,22 +7,9 @@ content="width=device-width, initial-scale=1, shrink-to-fit=no" /> - - - React App + React Fundamentals ⚛ @@ -86,15 +65,5 @@ You need to enable JavaScript to run this app.
- diff --git a/src/app.js b/src/app.js index d180ed755..4baf10214 100644 --- a/src/app.js +++ b/src/app.js @@ -1,40 +1,45 @@ import React from 'react' -import {Router, Link} from '@reach/router' +import { + BrowserRouter as Router, + Switch, + Route, + Link, + useParams, +} from 'react-router-dom' import {createBrowserHistory} from 'history' +import preval from 'preval.macro' +import pkg from '../package.json' -const history = createBrowserHistory() +const {title: projectTitle} = pkg -const files = ['05', '06', '07', '08', '09'] +if (!projectTitle) { + throw new Error('The package.json must have a title!') +} -const pages = files.reduce((p, filename, index, fullArray) => { - const final = require(`./exercises-final/${filename}.js`) - Object.assign(final, { - previous: fullArray[index - 1], - next: fullArray[index + 1], - isolatedPath: `/isolated/exercises-final/${filename}`, - }) - const exercise = require(`./exercises/${filename}.js`) - Object.assign(exercise, { - previous: fullArray[index - 1], - next: fullArray[index + 1], - isolatedPath: `/isolated/exercises/${filename}`, - }) - p[filename] = { - exercise, - final, - title: final.default.title, - } - return p -}, {}) +const exerciseInfo = preval`module.exports = require('./load-exercises')` -const filesAndTitles = files.map(filename => ({ - title: pages[filename].title, - filename, -})) +for (const infoKey in exerciseInfo) { + const info = exerciseInfo[infoKey] + info.exercise.Component = React.lazy(() => + import(`./exercises/${infoKey}.js`), + ) + info.final.Component = React.lazy(() => + import(`./exercises-final/${infoKey}.js`), + ) +} + +const history = createBrowserHistory() +function handleAnchorClick(event) { + if (event.metaKey || event.shiftKey) { + return + } + event.preventDefault() + history.push(event.target.getAttribute('href')) +} function ComponentContainer({label, ...props}) { return ( -
+

{label}

+ {`Extra Credits: `} + {Object.entries(extraCreditTitles).map(([id, title], index, array) => ( + + + {title} + + {array.length - 1 === index ? null : ' | '} + + ))} +
+ ) +} + +function ExerciseContainer() { + let {exerciseId} = useParams() const { - exercise: {default: Exercise}, - final: {default: Final}, - } = pages[exerciseId] + title, + exercise, + final, + exercise: {Component: Exercise}, + final: {Component: Final}, + } = exerciseInfo[exerciseId] return (
-

{Final.title}

+

{title}

Exercise} + label={ + + Exercise + + } > Final Version} + label={ + + Final + + } > +
) } function NavigationFooter({exerciseId, type}) { - const current = pages[exerciseId] + const current = exerciseInfo[exerciseId] let suffix = '' - let Usage = current.final + let info = current.final if (type === 'exercise') { suffix = '/exercise' - Usage = current.exercise + info = current.exercise } else if (type === 'final') { suffix = '/final' } @@ -102,9 +144,9 @@ function NavigationFooter({exerciseId, type}) { }} >
- {Usage.previous ? ( - - {pages[Usage.previous].title}{' '} + {info.previous ? ( + + {exerciseInfo[info.previous].title}{' '} 👈 @@ -115,12 +157,12 @@ function NavigationFooter({exerciseId, type}) { Home
- {Usage.next ? ( - + {info.next ? ( + 👉 {' '} - {pages[Usage.next].title} + {exerciseInfo[info.next].title} ) : null}
@@ -128,86 +170,37 @@ function NavigationFooter({exerciseId, type}) { ) } -function FullPage({type, exerciseId}) { - const page = pages[exerciseId] - const {default: Usage, isolatedPath} = pages[exerciseId][type] - return ( -
-
- - - 👈 - - Exercise Page - - isolated -
-
-

{page.title}

-
-
- -
- -
- ) -} - -function Isolated({loader}) { - const Component = React.useMemo(() => React.lazy(loader), [loader]) +function Home() { return (
+

{projectTitle}

- -
-
- ) -} - -function Home() { - return ( -
-

React Fundamentals

-
- {filesAndTitles.map(({title, filename}) => { - return ( -
- {filename} - {'. '} - {title}{' '} - - (exercise){' '} - (final) - -
- ) - })} + {Object.entries(exerciseInfo).map( + ([filename, {title, final, exercise}]) => { + return ( +
+ {filename} + {'. '} + {title}{' '} + + + (exercise) + {' '} + + (final) + + +
+ ) + }, + )}
) @@ -237,37 +230,112 @@ function NotFound() { function Routes() { return ( - - - - - + + + + + + + + + + + ) } +// cache +const lazyComps = {final: {}, exercise: {}, examples: {}} + +function useIsolatedComponent({pathname}) { + const isIsolated = pathname.startsWith('/isolated') + const isFinal = pathname.includes('/exercises-final/') + const isExercise = pathname.includes('/exercises/') + const isExample = pathname.includes('/examples/') + const moduleName = isIsolated + ? pathname.split(/\/isolated\/.*?\//).slice(-1)[0] + : null + const IsolatedComponent = React.useMemo(() => { + if (!moduleName) { + return null + } + if (isFinal) { + return (lazyComps.final[moduleName] = + lazyComps.final[moduleName] || + React.lazy(() => import(`./exercises-final/${moduleName}.js`))) + } else if (isExercise) { + return (lazyComps.exercise[moduleName] = + lazyComps.exercise[moduleName] || + React.lazy(() => import(`./exercises/${moduleName}.js`))) + } else if (isExample) { + return (lazyComps.examples[moduleName] = + lazyComps.examples[moduleName] || + React.lazy(() => import(`./examples/${moduleName}.js`))) + } + }, [isExample, isExercise, isFinal, moduleName]) + return moduleName ? IsolatedComponent : null +} + +function useExerciseTitle({pathname}) { + const isIsolated = pathname.startsWith('/isolated') + const isFinal = pathname.includes('/exercises-final/') + const isExercise = pathname.includes('/exercises/') + const exerciseName = isIsolated + ? pathname.split(/\/isolated\/.*?\//).slice(-1)[0] + : pathname.split('/').slice(-1)[0] + + React.useEffect(() => { + document.title = [ + projectTitle, + exerciseName, + isExercise ? 'Exercise' : null, + isFinal ? 'Final' : null, + ] + .filter(Boolean) + .join(' | ') + }, [exerciseName, isExercise, isFinal]) +} + +function useLocationBodyClassName({pathname}) { + const className = pathname.replace(/\//g, '_') + React.useEffect(() => { + document.body.classList.add(className) + return () => document.body.classList.remove(className) + }, [className]) +} + // The reason we don't put the Isolated components as regular routes // and do all this complex stuff instead is so the React DevTools component // tree is as small as possible to make it easier for people to figure // out what is relevant to the example. -function App() { +function MainApp() { const [location, setLocation] = React.useState(history.location) - React.useEffect(() => { - return history.listen(l => setLocation(l)) - }, []) - const {pathname} = location - let ui = - if (pathname.startsWith('/isolated')) { - const moduleName = pathname.split('/').slice(-1)[0] - if (pathname.includes('-final')) { - ui = ( - import(`./exercises-final/${moduleName}.js`)} /> - ) - } else { - ui = import(`./exercises/${moduleName}.js`)} /> - } - } - return Loading...
}>{ui} + React.useEffect(() => history.listen(l => setLocation(l)), []) + useExerciseTitle(location) + useLocationBodyClassName(location) + + const IsolatedComponent = useIsolatedComponent(location) + + return ( + + Loading... +
+ } + > + {IsolatedComponent ? ( +
+
+ +
+
+ ) : ( + + )} + + ) } -export default App +export default MainApp diff --git a/src/examples/example.js b/src/examples/example.js new file mode 100644 index 000000000..0c9893e06 --- /dev/null +++ b/src/examples/example.js @@ -0,0 +1,2 @@ +export default () => + 'Anything you put in files in this directory will be accessible at /isolated/examples/' diff --git a/src/exercises-final/01.extra-1.html b/src/exercises-final/01-extra.1.html similarity index 100% rename from src/exercises-final/01.extra-1.html rename to src/exercises-final/01-extra.1.html diff --git a/src/exercises-final/02.extra-1.html b/src/exercises-final/02-extra.1.html similarity index 73% rename from src/exercises-final/02.extra-1.html rename to src/exercises-final/02-extra.1.html index b323fc580..7a4025985 100644 --- a/src/exercises-final/02.extra-1.html +++ b/src/exercises-final/02-extra.1.html @@ -3,8 +3,8 @@
- - + + - + + - - + + + - - + + + - - + + + - - + + + - - + + + - - + + + - + + - - + + +