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'}
+
+ {/*
*/}
+
+
+
+ );
+}
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Mobile menu button */}
+
+ {/* Icon when menu is closed. */}
+ {/* Menu open: "hidden", Menu closed: "block" */}
+
+
+
+ {/* Icon when menu is open. */}
+ {/* Menu open: "block", Menu closed: "hidden" */}
+
+
+
+
+
+
+
+ {/*
+ Mobile menu, toggle classes based on menu state.
+
+ Menu open: "block", Menu closed: "hidden"
+ */}
+
+
+
+
+
+
+
+
+
+ Tom Cook
+
+
+ tom@example.com
+
+
+
+
+
+
+
+ );
+}
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."`}
+
+
+
+
+
+
+ {lastViewedModuleURL
+ ? `Continue: ${lastViewedModuleLabel}`
+ : `Get Started: 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 (
-
+
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Mobile menu button */}
-
- {/* Icon when menu is closed. */}
- {/* Menu open: "hidden", Menu closed: "block" */}
-
-
-
- {/* Icon when menu is open. */}
- {/* Menu open: "block", Menu closed: "hidden" */}
-
-
-
-
-
-
-
- {/*
- Mobile menu, toggle classes based on menu state.
-
- Menu open: "block", Menu closed: "hidden"
- */}
-
-
-
-
-
-
-
-
-
- Tom Cook
-
-
- tom@example.com
-
-
-
-
-
-
-
+
- {/*
*/}
- {/*
*/}
- {/*
*/}
- {/* Welcome to the USACO Guide!*/}
- {/* */}
- {/*
*/}
- {/*
*/}
- {/* Get started on the first module, "Using this Guide."*/}
- {/*
*/}
- {/*
*/}
- {/*
*/}
- {/*
*/}
- {/*
*/}
-
-
-
-
- Welcome Back!
-
-
-
- Pick up where you left off. Your last viewed module was
- Input & Output.
-
-
-
-
-
-
- Continue: Input & Output
-
-
-
-
-
+
-
-
-
-
- Active Problems
-
-
-
+ {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 (