add checkbox status to problems, closes #56

This commit is contained in:
Nathan Wang 2020-07-17 00:50:36 -07:00
parent dfaf9d11b6
commit afd24c474b
17 changed files with 249 additions and 115 deletions

View file

@ -25,6 +25,10 @@ export class Problem {
public difficulty: 'Very Easy' | 'Easy' | 'Normal' | 'Hard' | 'Very Hard' | 'Insane';
public isIntro: boolean;
get uniqueID() {
return this.url;
}
constructor(
public source: string,
public name: string,

View file

@ -2,10 +2,10 @@ import './src/styles/main.css';
import './src/styles/anchor.css';
import * as React from 'react';
import MDXProvider from './src/components/markdown/MDXProvider';
import { UserSettingsProvider } from './src/context/UserSettingsContext';
import { UserDataProvider } from './src/context/UserDataContext';
export const wrapRootElement = ({ element }) => (
<MDXProvider>
<UserSettingsProvider>{element}</UserSettingsProvider>
<UserDataProvider>{element}</UserDataProvider>
</MDXProvider>
);

View file

@ -27,7 +27,7 @@ export const plugins = [
resolve: `gatsby-remark-autolink-headers`,
options: {
// icon source: https://joshwcomeau.com/
icon: `<svg fill="none" height="24" width="24" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
icon: `<svg fill="none" height="24" width="24" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
},
},
{
@ -53,7 +53,7 @@ export const plugins = [
resolve: `gatsby-remark-autolink-headers`,
options: {
// icon source: https://joshwcomeau.com/
icon: `<svg fill="none" height="24" width="24" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
icon: `<svg fill="none" height="24" width="24" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" style="display: inline-block; vertical-align: middle;"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>`,
},
},
],

View file

@ -1,9 +1,9 @@
import * as React from 'react';
import MDXProvider from './src/components/markdown/MDXProvider';
import { UserSettingsProvider } from './src/context/UserSettingsContext';
import { UserDataProvider } from './src/context/UserDataContext';
export const wrapRootElement = ({ element }) => (
<MDXProvider>
<UserSettingsProvider>{element}</UserSettingsProvider>
<UserDataProvider>{element}</UserDataProvider>
</MDXProvider>
);

View file

@ -1,6 +1,6 @@
import * as React from 'react';
import Transition from '../Transition';
import { ModuleProgressOptions } from '../../context/UserSettingsContext';
import { ModuleProgressOptions } from '../../context/UserDataContext';
const MarkCompleteButton = ({
state,

View file

@ -14,7 +14,7 @@ import ContactUsSlideover from '../ContactUsSlideover';
import MarkCompleteButton from './MarkCompleteButton';
import ModuleConfetti from './ModuleConfetti';
import TextTooltip from '../tooltip/TextTooltip';
import UserSettingsContext from '../../context/UserSettingsContext';
import UserDataContext, { UserLang } from '../../context/UserDataContext';
import { NavLinkGroup, SidebarNav } from './SidebarNav/SidebarNav';
import { graphqlToModuleLinks } from '../../utils';
import ModuleLayoutContext from '../../context/ModuleLayoutContext';
@ -114,21 +114,19 @@ const SidebarBottomButtons = ({ onContactUs }) => {
java: 'Java',
py: 'Python',
};
const nextLang = {
const nextLang: { [key: string]: UserLang } = {
showAll: 'cpp',
cpp: 'java',
java: 'py',
py: 'cpp',
};
const userSettings = useContext(UserSettingsContext);
const userSettings = useContext(UserDataContext);
return (
<>
<div className="flex-shrink-0 border-t border-gray-200 flex">
<button
className="group flex-1 flex items-center p-4 text-sm leading-5 font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50 focus:outline-none focus:bg-gray-100 transition ease-in-out duration-150"
onClick={() =>
userSettings.setPrimaryLang(nextLang[userSettings.primaryLang])
}
onClick={() => userSettings.setLang(nextLang[userSettings.lang])}
>
<svg
className="mr-4 h-6 w-6 text-gray-400 group-hover:text-gray-500 group-focus:text-gray-500 transition ease-in-out duration-150"
@ -141,7 +139,7 @@ const SidebarBottomButtons = ({ onContactUs }) => {
>
<path d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
Language: {languages[userSettings.primaryLang]}
Language: {languages[userSettings.lang]}
</button>
</div>
<div className="flex-shrink-0 border-t border-gray-200 flex">
@ -270,9 +268,7 @@ export default function ModuleLayout({
module: ModuleInfo;
children: React.ReactNode;
}) {
const { userProgress, setModuleProgress, primaryLang } = useContext(
UserSettingsContext
);
const { userProgress, setModuleProgress, lang } = useContext(UserDataContext);
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
const [isContactUsActive, setIsContactUsActive] = useState(false);
const [isConfettiActive, setIsConfettiActive] = useState(false);
@ -280,7 +276,7 @@ export default function ModuleLayout({
(userProgress && userProgress[module.id]) || 'Not Started';
const tableOfContents =
primaryLang in module.toc ? module.toc[primaryLang] : module.toc['cpp'];
lang in module.toc ? module.toc[lang] : module.toc['cpp'];
const data = useStaticQuery(graphql`
query {
@ -444,7 +440,7 @@ export default function ModuleLayout({
>
<div className="mx-auto">
<div className="flex justify-center">
<div className="flex-1 max-w-4xl px-4 sm:px-6 lg:px-8">
<div className="flex-1 max-w-4xl px-4 sm:px-6 lg:px-8 w-0">
<div className="hidden lg:block">
<NavBar />
</div>
@ -531,7 +527,7 @@ export default function ModuleLayout({
<NavBar alignNavButtonsRight={false} />
</div>
</div>
<div className="hidden xl:block ml-6 w-64 mt-48">
<div className="hidden xl:block ml-6 w-64 mt-48 flex-shrink-0">
<TableOfContentsSidebar tableOfContents={tableOfContents} />
</div>
</div>

View file

@ -5,7 +5,7 @@ import styled from 'styled-components';
import tw from 'twin.macro';
import { useContext } from 'react';
import ModuleLayoutContext from '../../../context/ModuleLayoutContext';
import UserSettingsContext from '../../../context/UserSettingsContext';
import UserDataContext from '../../../context/UserDataContext';
const LinkWithProgress = styled.span`
${tw`block relative`}
@ -88,7 +88,7 @@ const ItemLink = ({ link }: { link: ModuleLinkInfo }) => {
}
}, [isActive]);
const { userProgress } = useContext(UserSettingsContext);
const { userProgress } = useContext(UserDataContext);
const progress = userProgress[link.id] || 'Not Started';
let lineColorStyle = tw`bg-gray-200`;

View file

@ -25,6 +25,7 @@ const TableOfContentsBlock = ({
curDepth = heading.depth;
links.push(
<Link
key={heading.slug}
to={'#' + heading.slug}
className="block mb-2 transition duration-150 ease-in-out text-gray-600 hover:underline hover:text-blue-600"
style={{

View file

@ -28,6 +28,7 @@ const TableOfContentsSidebar = ({
curDepth = heading.depth;
links.push(
<Link
key={heading.slug}
to={'#' + heading.slug}
className={
'block mb-1 text-sm transition duration-150 ease-in-out ' +

View file

@ -0,0 +1,50 @@
import * as React from 'react';
import Tooltip from './tooltip/Tooltip';
import { Problem } from '../../content/models';
import { useContext } from 'react';
import UserDataContext, {
NEXT_PROBLEM_STATUS,
ProblemStatus,
} from '../context/UserDataContext';
export default function ProblemStatusCheckbox({
problem,
}: {
problem: Problem;
}) {
const { problemStatus, setProblemStatus } = useContext(UserDataContext);
let status: ProblemStatus =
problemStatus[problem.uniqueID] || 'Not Attempted';
const icon: { [key in ProblemStatus]: React.ReactNode } = {
'Not Attempted': (
<span className="inline-block h-6 w-6 rounded-full bg-gray-200 cursor-pointer" />
),
Solving: (
<span className="inline-block h-6 w-6 rounded-full bg-yellow-300 cursor-pointer" />
),
Solved: (
<span className="inline-block h-6 w-6 rounded-full bg-green-500 cursor-pointer" />
),
"Can't Solve": (
<span className="inline-block h-6 w-6 rounded-full bg-red-500 cursor-pointer" />
),
Skipped: (
<span className="inline-block h-6 w-6 rounded-full bg-blue-300 cursor-pointer" />
),
};
const handleClick = () => {
setProblemStatus(problem, NEXT_PROBLEM_STATUS[status]);
};
return (
<Tooltip
content={status}
hideOnClick={false}
type="compact"
position="left"
>
<span onClick={handleClick} className="inline-block h-6 w-6">
{icon[status]}
</span>
</Tooltip>
);
}

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import UserSettingsContext from '../../context/UserSettingsContext';
import UserDataContext from '../../context/UserDataContext';
import { useContext } from 'react';
export const IncompleteSection = props => {

View file

@ -1,10 +1,10 @@
import * as React from 'react';
import UserSettingsContext from '../../context/UserSettingsContext';
import UserDataContext from '../../context/UserDataContext';
import { useContext } from 'react';
export const LanguageSection = props => {
const userSettings = useContext(UserSettingsContext);
let lang = userSettings.primaryLang;
const userSettings = useContext(UserDataContext);
let lang = userSettings.lang;
let sections = {};
React.Children.map(props.children, child => {

View file

@ -4,6 +4,7 @@ import Transition from '../Transition';
import Tooltip from '../tooltip/Tooltip';
import TextTooltip from '../tooltip/TextTooltip';
import { sourceTooltip } from './Resources';
import ProblemStatusCheckbox from '../ProblemStatusCheckbox';
type ProblemsListComponentProps = {
title?: string;
@ -22,6 +23,9 @@ export function ProblemsListComponent(props: ProblemsListComponentProps) {
<table className="w-full no-markdown">
<thead>
<tr>
<th className="pl-4 md:pl-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th className="pl-4 md:px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Source
</th>
@ -146,6 +150,9 @@ export function ProblemComponent(props: ProblemComponentProps) {
return (
<tr>
<td className="pl-4 md:pl-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500 font-medium text-center">
<ProblemStatusCheckbox problem={problem} />
</td>
<td className="pl-4 md:px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500 font-medium">
{sourceTooltip.hasOwnProperty(problem.source) ? (
<TextTooltip content={sourceTooltip[problem.source]}>

View file

@ -3,7 +3,7 @@ import Dots from '../Dots';
import Tooltip from '../tooltip/Tooltip';
import TextTooltip from '../tooltip/TextTooltip';
import { useContext } from 'react';
import UserSettingsContext from '../../context/UserSettingsContext';
import UserDataContext from '../../context/UserDataContext';
export function ResourcesListComponent(props) {
const embedded = props.embedded;
@ -92,13 +92,13 @@ export const sourceTooltip = {
};
export function ResourceComponent(props) {
const userSettings = useContext(UserSettingsContext);
const userSettings = useContext(UserDataContext);
const source = props.source;
let url = props.url;
if (!url) {
if (source === 'IUSACO') {
if (userSettings.primaryLang === 'java') {
if (userSettings.lang === 'java') {
url = 'https://darrenyao.com/usacobook/java.pdf';
} else {
url = 'https://darrenyao.com/usacobook/cpp.pdf';

View file

@ -15,11 +15,17 @@ const StyledTippy = styled(Tippy)`
font-weight: normal !important;
text-align: center;
& > .tippy-arrow::before {
${p =>
p.placement === 'top'
? 'border-top-color'
: 'border-bottom-color'}: #252f3f !important;
&[data-placement^='top'] > .tippy-arrow::before {
border-top-color: #252f3f !important;
}
&[data-placement^='bottom'] > .tippy-arrow::before {
border-bottom-color: #252f3f !important;
}
&[data-placement^='left'] > .tippy-arrow::before {
border-left-color: #252f3f !important;
}
&[data-placement^='right'] > .tippy-arrow::before {
border-right-color: #252f3f !important;
}
`;
@ -28,9 +34,21 @@ const AsteriskTippy = styled(StyledTippy)`
p.placement === 'top' ? 'translateY(10px)' : 'translateY(-7px)'};
`;
const Tooltip = ({ children, content, position = 'top', type = 'normal' }) => {
const CompactTippy = styled(StyledTippy)`
font-size: 0.875rem !important;
padding: 0rem;
`;
const Tooltip = ({
children,
content,
position = 'top',
type = 'normal',
...other
}) => {
let Component = StyledTippy;
if (type === 'asterisk') Component = AsteriskTippy;
else if (type === 'compact') Component = CompactTippy;
return (
<Component
content={content}
@ -38,6 +56,7 @@ const Tooltip = ({ children, content, position = 'top', type = 'normal' }) => {
theme="material"
duration={200}
placement={position}
{...other}
>
{children}
</Component>

View file

@ -0,0 +1,136 @@
import { createContext, useState } from 'react';
import * as React from 'react';
import { Problem } from '../../content/models';
export type ModuleProgress =
| 'Not Started'
| 'Reading'
| 'Practicing'
| 'Complete'
| 'Skipped';
export const ModuleProgressOptions: ModuleProgress[] = [
'Not Started',
'Reading',
'Practicing',
'Complete',
'Skipped',
];
export type UserProgress = { [key: string]: ModuleProgress };
export type UserLang = 'showAll' | 'cpp' | 'java' | 'py';
export type ProblemStatus =
| 'Not Attempted'
| 'Solving'
| 'Solved'
| "Can't Solve"
| 'Skipped';
export const NEXT_PROBLEM_STATUS: { [key in ProblemStatus]: ProblemStatus } = {
'Not Attempted': 'Solving',
Solving: 'Solved',
Solved: "Can't Solve",
"Can't Solve": 'Skipped',
Skipped: 'Not Attempted',
};
const UserDataContext = createContext<{
lang: UserLang;
setLang: (lang: UserLang) => void;
userProgress: UserProgress;
setModuleProgress: (moduleID: string, progress: ModuleProgress) => void;
problemStatus: { [key: string]: ProblemStatus };
setProblemStatus: (problem: Problem, status: ProblemStatus) => void;
}>({
lang: 'showAll',
setLang: null,
userProgress: null,
setModuleProgress: null,
problemStatus: null,
setProblemStatus: null,
});
const langKey = 'guide:userData:lang';
const getLangFromStorage = () => {
let stickyValue = window.localStorage.getItem(langKey);
let v = null;
try {
v = JSON.parse(stickyValue);
} catch (e) {
console.error("Couldn't parse user primary language", e);
}
if (v === 'cpp' || v === 'java' || v === 'py') return v;
return 'cpp';
};
const progressKey = 'guide:userData:progress';
const getProgressFromStorage = () => {
let stickyValue = window.localStorage.getItem(progressKey);
let v = {};
try {
v = JSON.parse(stickyValue);
} catch (e) {
console.error("Couldn't parse user progress", e);
}
return v || {};
};
const problemStatusKey = 'guide:userData:problemStatus';
const getProblemStatusFromStorage = () => {
let stickyValue = window.localStorage.getItem(problemStatusKey);
let v = {};
try {
v = JSON.parse(stickyValue);
} catch (e) {
console.error("Couldn't parse problem status", e);
}
return v || {};
};
export const UserDataProvider = ({ children }) => {
const [lang, setLang] = useState<UserLang>('showAll');
const [userProgress, setUserProgress] = useState<UserProgress>({});
const [problemStatus, setProblemStatus] = useState<{
[key: string]: ProblemStatus;
}>({});
React.useEffect(() => {
setLang(getLangFromStorage());
setUserProgress(getProgressFromStorage());
setProblemStatus(getProblemStatusFromStorage());
}, []);
return (
<UserDataContext.Provider
value={{
lang: lang as UserLang,
setLang: lang => {
window.localStorage.setItem(langKey, JSON.stringify(lang));
setLang(lang);
},
userProgress,
setModuleProgress: (moduleID: string, progress: ModuleProgress) => {
const newProgress = {
...getProgressFromStorage(),
[moduleID]: progress,
};
window.localStorage.setItem(progressKey, JSON.stringify(newProgress));
setUserProgress(newProgress);
},
problemStatus,
setProblemStatus: (problem, status) => {
const newStatus = {
...getProblemStatusFromStorage(),
[problem.uniqueID]: status,
};
window.localStorage.setItem(
problemStatusKey,
JSON.stringify(newStatus)
);
setProblemStatus(newStatus);
},
}}
>
{children}
</UserDataContext.Provider>
);
};
export default UserDataContext;

View file

@ -1,80 +0,0 @@
import { createContext, useState } from 'react';
import * as React from 'react';
export type ModuleProgress =
| 'Not Started'
| 'Reading'
| 'Practicing'
| 'Complete'
| 'Skipped';
export const ModuleProgressOptions: ModuleProgress[] = [
'Not Started',
'Reading',
'Practicing',
'Complete',
'Skipped',
];
export type UserProgress = { [key: string]: ModuleProgress };
export type UserLang = 'showAll' | 'cpp' | 'java' | 'py';
const UserSettingsContext = createContext<{
primaryLang: UserLang;
setPrimaryLang: (lang: string) => void;
userProgress: UserProgress;
setModuleProgress: (moduleID: string, progress: ModuleProgress) => void;
}>({
primaryLang: 'showAll',
setPrimaryLang: null,
userProgress: null,
setModuleProgress: null,
});
export const UserSettingsProvider = ({ children }) => {
const langKey = 'guide:userSettings:primaryLang';
const progressKey = 'guide:userSettings:progress';
const [primaryLang, setPrimaryLang] = useState('showAll');
const [userProgress, setUserProgress] = useState({});
React.useEffect(() => {
let stickyValue = window.localStorage.getItem(langKey);
let v = null;
try {
v = JSON.parse(stickyValue);
} catch (e) {
console.error("Couldn't parse user primary language", e);
}
if (v === 'cpp' || v === 'java' || v === 'py') setPrimaryLang(v);
else setPrimaryLang('cpp');
stickyValue = window.localStorage.getItem(progressKey);
v = {};
try {
v = JSON.parse(stickyValue);
} catch (e) {
console.error("Couldn't parse user progress", e);
}
setUserProgress(v || {});
}, []);
return (
<UserSettingsContext.Provider
value={{
primaryLang: primaryLang as UserLang,
setPrimaryLang: lang => {
window.localStorage.setItem(langKey, JSON.stringify(lang));
setPrimaryLang(lang);
},
userProgress,
setModuleProgress: (moduleID: string, progress: ModuleProgress) => {
const newProgress = { ...userProgress, [moduleID]: progress };
window.localStorage.setItem(progressKey, JSON.stringify(newProgress));
setUserProgress(newProgress);
},
}}
>
{children}
</UserSettingsContext.Provider>
);
};
export default UserSettingsContext;