diff --git a/content/ordering.ts b/content/ordering.ts index 9aa1212..09567a0 100644 --- a/content/ordering.ts +++ b/content/ordering.ts @@ -304,3 +304,15 @@ SECTIONS.forEach(section => { }); export { moduleIDToSectionMap }; + +let moduleIDToURLMap: {[key: string]: string} = {}; + +SECTIONS.forEach(section => { + MODULE_ORDERING[section].forEach(category => { + category.items.forEach(moduleID => { + moduleIDToURLMap[moduleID] = `/${section}/${moduleID}`; + }) + }); +}); + +export { moduleIDToURLMap }; diff --git a/src/components/Dashboard/ActiveItems.tsx b/src/components/Dashboard/ActiveItems.tsx index e69de29..389d883 100644 --- a/src/components/Dashboard/ActiveItems.tsx +++ b/src/components/Dashboard/ActiveItems.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import { Link } from 'gatsby'; + +type ActiveItemStatus = 'Solving' | 'Skipped' | 'In Progress'; + +export type ActiveItem = { + label: string; + status: ActiveItemStatus; + url: string; +}; + +const statusClasses: { [key in ActiveItemStatus]: string } = { + Solving: 'bg-yellow-100 text-yellow-800', + Skipped: 'bg-gray-100 text-gray-800', + 'In Progress': 'bg-green-100 text-green-800', +}; + +export default function ActiveItems({ + type, + items, +}: { + type: 'problems' | 'modules'; + items: ActiveItem[]; +}) { + return ( +
+
+

+ Active {type === 'problems' ? 'Problems' : 'Modules'} +

+ {/*
*/} + {/*

*/} + {/* */} + {/* Intro: Input & Output*/} + {/* */} + {/* Practicing*/} + {/* */} + {/* */} + {/*

*/} + {/*

*/} + {/* */} + {/* Intro: Expected Knowledge*/} + {/* */} + {/* Skipped*/} + {/* */} + {/* */} + {/*

*/} + {/*
*/} +
+ {items.map((item, idx) => ( +

+ + {item.label} + + {item.status} + + +

+ ))} + {/*

*/} + {/* */} + {/* Longest Common Subsequence*/} + {/* */} + {/* Skipped*/} + {/* */} + {/* */} + {/*

*/} +
+
+
+ ); +} diff --git a/src/components/Dashboard/DashboardNav.tsx b/src/components/Dashboard/DashboardNav.tsx index e69de29..28e1a16 100644 --- a/src/components/Dashboard/DashboardNav.tsx +++ b/src/components/Dashboard/DashboardNav.tsx @@ -0,0 +1,213 @@ +import * as React from 'react'; +// @ts-ignore +import logo from '../../assets/logo.svg'; +// @ts-ignore +import logoSquare from '../../assets/logo-square.png'; + +export default function DashboardNav() { + return ( + + ); +} diff --git a/src/components/Dashboard/WelcomeBackBanner.tsx b/src/components/Dashboard/WelcomeBackBanner.tsx index e69de29..3061cd1 100644 --- a/src/components/Dashboard/WelcomeBackBanner.tsx +++ b/src/components/Dashboard/WelcomeBackBanner.tsx @@ -0,0 +1,43 @@ +import { Link } from 'gatsby'; +import * as React from 'react'; + +export default function WelcomeBackBanner({ + lastViewedModuleURL, + lastViewedModuleLabel, +}) { + return ( +
+ +
+

+ {lastViewedModuleURL + ? 'Welcome Back!' + : 'Welcome to the USACO Guide!'} +

+
+

+ {lastViewedModuleURL + ? `Pick up where you left off. Your last viewed module was ${lastViewedModuleLabel}.` + : `Get started on the first module, "Using This Guide."`} +

+
+
+
+ + + +
+ +
+ ); +} diff --git a/src/context/UserDataContext.tsx b/src/context/UserDataContext.tsx index cc43758..2f7f3ed 100644 --- a/src/context/UserDataContext.tsx +++ b/src/context/UserDataContext.tsx @@ -24,6 +24,9 @@ const UserDataContext = createContext<{ problem: Problem, status: ProblemProgress ) => void; + + lastViewedModule: string; + setLastViewedModule: (moduleID: string) => void; }>({ lang: 'showAll', setLang: null, @@ -31,6 +34,8 @@ const UserDataContext = createContext<{ setModuleProgress: null, userProgressOnProblems: null, setUserProgressOnProblems: null, + lastViewedModule: null, + setLastViewedModule: null, }); const langKey = 'guide:userData:lang'; @@ -70,6 +75,18 @@ const getProblemStatusFromStorage = () => { return v || {}; }; +const lastViewedModuleKey = 'guide:userData:lastViewedModule'; +const getLastViewedModuleFromStorage = () => { + let stickyValue = window.localStorage.getItem(lastViewedModuleKey); + let v = null; + try { + v = JSON.parse(stickyValue); + } catch (e) { + console.error("Couldn't parse last viewed module", e); + } + return v || null; +}; + export const UserDataProvider = ({ children }) => { const [lang, setLang] = useState('showAll'); const [userProgress, setUserProgress] = useState<{ @@ -78,44 +95,57 @@ export const UserDataProvider = ({ children }) => { const [problemStatus, setProblemStatus] = useState<{ [key: string]: ProblemProgress; }>({}); + const [lastViewedModule, setLastViewedModule] = useState(null); React.useEffect(() => { setLang(getLangFromStorage()); setUserProgress(getProgressFromStorage()); setProblemStatus(getProblemStatusFromStorage()); + setLastViewedModule(getLastViewedModuleFromStorage()); }, []); + const userData = React.useMemo( + () => ({ + lang: lang as UserLang, + setLang: lang => { + window.localStorage.setItem(langKey, JSON.stringify(lang)); + setLang(lang); + }, + userProgressOnModules: userProgress, + setModuleProgress: (moduleID: string, progress: ModuleProgress) => { + const newProgress = { + ...getProgressFromStorage(), + [moduleID]: progress, + }; + window.localStorage.setItem(progressKey, JSON.stringify(newProgress)); + setUserProgress(newProgress); + }, + userProgressOnProblems: problemStatus, + setUserProgressOnProblems: (problem, status) => { + const newStatus = { + ...getProblemStatusFromStorage(), + [problem.uniqueID]: status, + }; + window.localStorage.setItem( + problemStatusKey, + JSON.stringify(newStatus) + ); + setProblemStatus(newStatus); + }, + lastViewedModule, + setLastViewedModule: moduleID => { + window.localStorage.setItem( + lastViewedModuleKey, + JSON.stringify(moduleID) + ); + setLastViewedModule(moduleID); + }, + }), + [lang, userProgress, problemStatus, lastViewedModule] + ); + return ( - { - window.localStorage.setItem(langKey, JSON.stringify(lang)); - setLang(lang); - }, - userProgressOnModules: userProgress, - setModuleProgress: (moduleID: string, progress: ModuleProgress) => { - const newProgress = { - ...getProgressFromStorage(), - [moduleID]: progress, - }; - window.localStorage.setItem(progressKey, JSON.stringify(newProgress)); - setUserProgress(newProgress); - }, - userProgressOnProblems: problemStatus, - setUserProgressOnProblems: (problem, status) => { - const newStatus = { - ...getProblemStatusFromStorage(), - [problem.uniqueID]: status, - }; - window.localStorage.setItem( - problemStatusKey, - JSON.stringify(newStatus) - ); - setProblemStatus(newStatus); - }, - }} - > + {children} ); diff --git a/src/pages/dashboard.tsx b/src/pages/dashboard.tsx index ca0e76e..460c303 100644 --- a/src/pages/dashboard.tsx +++ b/src/pages/dashboard.tsx @@ -1,347 +1,78 @@ import * as React from 'react'; -import { Link, PageProps } from 'gatsby'; +import { graphql, Link, PageProps } from 'gatsby'; import Layout from '../components/layout'; import SEO from '../components/seo'; import { useState } from 'react'; -// @ts-ignore -import logo from '../assets/logo.svg'; -// @ts-ignore -import logoSquare from '../assets/logo-square.png'; import SectionProgress from '../components/Dashboard/SectionProgress'; import SectionProgressBar from '../components/Dashboard/SectionProgressBar'; +import UserDataContext from '../context/UserDataContext'; +import WelcomeBackBanner from '../components/Dashboard/WelcomeBackBanner'; +import { + moduleIDToSectionMap, + moduleIDToURLMap, + SECTION_LABELS, +} from '../../content/ordering'; +import DashboardNav from '../components/Dashboard/DashboardNav'; +import ActiveItems, { ActiveItem } from '../components/Dashboard/ActiveItems'; export default function DashboardPage(props: PageProps) { const [isMobileNavOpen, setIsMobileNavOpen] = useState(false); + const { modules } = props.data as any; + const moduleIDToName = modules.edges.reduce((acc, cur) => { + acc[cur.node.frontmatter.id] = cur.node.frontmatter.title; + return acc; + }, {}); + const { + lastViewedModule: lastViewedModuleID, + userProgressOnModules, + } = React.useContext(UserDataContext); + const lastViewedModuleURL = moduleIDToURLMap[lastViewedModuleID]; + const activeModules: ActiveItem[] = React.useMemo(() => { + return Object.keys(userProgressOnModules) + .filter( + x => + userProgressOnModules[x] === 'Reading' || + userProgressOnModules[x] === 'Practicing' || + userProgressOnModules[x] === 'Skipped' + ) + .map(x => ({ + label: `${SECTION_LABELS[moduleIDToSectionMap[x]]}: ${ + moduleIDToName[x] + }`, + url: moduleIDToURLMap[x], + status: + userProgressOnModules[x] === 'Skipped' ? 'Skipped' : 'In Progress', + })); + }, [userProgressOnModules]); + const activeProblems: ActiveItem[] = []; return ( - +
- +
- {/*
*/} - {/*
*/} - {/*

*/} - {/* Welcome to the USACO Guide!*/} - {/*

*/} - {/*
*/} - {/*

*/} - {/* Get started on the first module, "Using this Guide."*/} - {/*

*/} - {/*
*/} - {/*
*/} - {/* */} - {/* Get Started! →*/} - {/* */} - {/*
*/} - {/*
*/} - {/*
*/} -
- -
-

- Welcome Back! -

-
-

- Pick up where you left off. Your last viewed module was - Input & Output. -

-
-
-
- - - -
- -
+
-
-
- + {activeProblems.length > 0 && ( +
+
-
-
-
- + )} + {activeModules.length > 0 && ( +
+
-
+ )}
@@ -460,3 +191,18 @@ export default function DashboardPage(props: PageProps) { ); } + +export const pageQuery = graphql` + query { + modules: allMdx { + edges { + node { + frontmatter { + title + id + } + } + } + } + } +`; diff --git a/src/templates/moduleTemplate.tsx b/src/templates/moduleTemplate.tsx index c72ad71..5cb061e 100644 --- a/src/templates/moduleTemplate.tsx +++ b/src/templates/moduleTemplate.tsx @@ -7,11 +7,17 @@ import { SECTION_LABELS } from '../../content/ordering'; import { graphqlToModuleInfo } from '../utils'; import SEO from '../components/seo'; import ModuleLayout from '../components/ModuleLayout/ModuleLayout'; +import { useContext } from 'react'; +import UserDataContext from '../context/UserDataContext'; export default function Template(props) { const { mdx } = props.data; // data.markdownRemark holds your post data const { body } = mdx; const module = React.useMemo(() => graphqlToModuleInfo(mdx), [mdx]); + const { setLastViewedModule } = useContext(UserDataContext); + React.useEffect(() => { + setLastViewedModule(module.id); + }, []); return (