overhaul syllabus
This commit is contained in:
parent
ea793dae09
commit
24f096dd13
|
@ -1,16 +1,18 @@
|
|||
// Section -> Category -> Module
|
||||
// Section -> Chapter -> Module
|
||||
|
||||
export type SectionID = "intro" | "bronze" | "silver" | "gold" | "plat" | "adv";
|
||||
|
||||
export type Category = {
|
||||
export type Chapter = {
|
||||
name: string;
|
||||
items: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const MODULE_ORDERING: {[key in SectionID]: Category[]} = {
|
||||
const MODULE_ORDERING: {[key in SectionID]: Chapter[]} = {
|
||||
"intro": [
|
||||
{
|
||||
name: "About This Guide",
|
||||
description: "In this first chapter, you'll learn about how this guide is structured and how best to use this guide.",
|
||||
items: [
|
||||
"using-this-guide",
|
||||
"modules",
|
||||
|
|
|
@ -21,4 +21,5 @@ The following is written for individuals without front-end development experienc
|
|||
## Credits
|
||||
|
||||
- Confetti taken from Josh Comeau: https://github.com/joshwcomeau/react-europe-talk-2018
|
||||
- Lots of inspiration from Josh Comeau's blog and Gatsbyjs documentation site
|
||||
- Lots of inspiration from Josh Comeau's blog and Gatsbyjs documentation site
|
||||
- Syllabus template from https://www.howtographql.com/
|
159
src/components/Dashboard/ModuleLink.tsx
Normal file
159
src/components/Dashboard/ModuleLink.tsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
import * as React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
import { ModuleLinkInfo } from '../../models/module';
|
||||
import { useContext } from 'react';
|
||||
import ModuleLayoutContext from '../../context/ModuleLayoutContext';
|
||||
import UserDataContext from '../../context/UserDataContext';
|
||||
import { Link } from 'gatsby';
|
||||
|
||||
const LinkWithProgress = styled.span`
|
||||
${tw`block relative`}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
//left: calc(-3rem - 12px); // -(3rem padding plus half of width)
|
||||
//top: calc(1.5rem - 12px); // half of (1.5 + 1.5padding) rem minus half of height
|
||||
//height: 24px;
|
||||
//width: 24px;
|
||||
${({ small }) => css`
|
||||
left: calc(-1.75rem - ${
|
||||
small ? '8px' : '10px'
|
||||
}); // -(3rem padding plus half of width)
|
||||
// prettier-ignore
|
||||
top: calc(1.5rem - ${
|
||||
small ? '8px' : '10px'
|
||||
}); // half of (1.5 + 1.5padding) rem minus half of height
|
||||
height: ${small ? '16px' : '20px'};
|
||||
width: ${small ? '16px' : '20px'};
|
||||
`}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
${({ small }) => css`
|
||||
// prettier-ignore
|
||||
left: calc(-3rem - ${
|
||||
small ? '8px' : '10px'
|
||||
}); // -(3rem padding plus half of width)
|
||||
`}
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-radius: 100%;
|
||||
|
||||
${props => props.dotColorStyle}
|
||||
${({ small }) => small && tw`border-2 border-gray-200 bg-white`}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
display: block;
|
||||
left: calc(-1.75rem - 1px);
|
||||
@media (min-width: 768px) {
|
||||
left: calc(-3rem - 1px); // -(3rem padding plus half of width)
|
||||
}
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
${props => props.lineColorStyle};
|
||||
}
|
||||
|
||||
&:first-of-type::before {
|
||||
top: 22px;
|
||||
}
|
||||
|
||||
&:last-of-type::before {
|
||||
bottom: calc(100% - 22px);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.div`
|
||||
${tw`focus:outline-none transition ease-in-out duration-150 text-gray-800 hover:text-blue-700 text-xl leading-6 py-3`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
left: calc(-1.75rem - 11px);
|
||||
@media (min-width: 768px) {
|
||||
left: calc(-3rem - 11px); // -(3rem padding plus half of width)
|
||||
}
|
||||
top: calc(1.5rem - 11px); // half of 1.5rem minus half of height
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
position: absolute;
|
||||
transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1) 0s;
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: ${({ showDot }) => (showDot ? 'scale(1)' : 'scale(0.1)')};
|
||||
border-radius: 100%;
|
||||
z-index: 1;
|
||||
${({ dotColorStyle }) => dotColorStyle}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
transform: scale(1);
|
||||
${tw`bg-blue-600`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ModuleLink = ({ link }: { link: ModuleLinkInfo }) => {
|
||||
const { userProgressOnModules } = useContext(UserDataContext);
|
||||
const progress = userProgressOnModules[link.id] || 'Not Started';
|
||||
|
||||
let lineColorStyle = tw`bg-gray-200`;
|
||||
let dotColorStyle = tw`bg-white`;
|
||||
let activeTextStyle = tw`text-blue-700 font-medium`;
|
||||
|
||||
// if (isActive) {
|
||||
// lineColorStyle = tw`bg-blue-700`;
|
||||
// dotColorStyle = tw`bg-blue-700`;
|
||||
// }
|
||||
|
||||
if (progress === 'Reading') {
|
||||
lineColorStyle = tw`bg-yellow-400`;
|
||||
dotColorStyle = tw`bg-yellow-400`;
|
||||
activeTextStyle = tw`text-yellow-700 font-medium`;
|
||||
} else if (progress === 'Practicing') {
|
||||
lineColorStyle = tw`bg-orange-400`;
|
||||
dotColorStyle = tw`bg-orange-400`;
|
||||
activeTextStyle = tw`text-orange-700 font-medium`;
|
||||
} else if (progress === 'Complete') {
|
||||
lineColorStyle = tw`bg-green-400`;
|
||||
dotColorStyle = tw`bg-green-400`;
|
||||
activeTextStyle = tw`text-green-700 font-medium`;
|
||||
} else if (progress === 'Skipped') {
|
||||
lineColorStyle = tw`bg-blue-300`;
|
||||
dotColorStyle = tw`bg-blue-300`;
|
||||
activeTextStyle = tw`text-blue-700 font-medium`;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkWithProgress
|
||||
lineColorStyle={lineColorStyle}
|
||||
dotColorStyle={dotColorStyle}
|
||||
small={progress === 'Not Started'}
|
||||
>
|
||||
<Link to={link.url}>
|
||||
<StyledLink
|
||||
dotColorStyle={dotColorStyle === tw`bg-gray-200`}
|
||||
className="group"
|
||||
>
|
||||
<p className="text-gray-600 group-hover:text-blue-800 transition duration-150 ease-in-out">
|
||||
{link.title}
|
||||
</p>
|
||||
<p className="block text-sm text-gray-400 group-hover:text-blue-700 transition duration-150 ease-in-out">
|
||||
{link.description}
|
||||
</p>
|
||||
</StyledLink>
|
||||
</Link>
|
||||
</LinkWithProgress>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModuleLink;
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '../../models/module';
|
||||
import { graphql, Link, useStaticQuery } from 'gatsby';
|
||||
import MODULE_ORDERING, {
|
||||
Category,
|
||||
Chapter,
|
||||
SECTION_LABELS,
|
||||
} from '../../../content/ordering';
|
||||
import ModuleFrequencyDots from './ModuleFrequencyDots';
|
||||
|
@ -20,7 +20,7 @@ import ModuleConfetti from './ModuleConfetti';
|
|||
import TextTooltip from '../Tooltip/TextTooltip';
|
||||
import UserDataContext, { UserLang } from '../../context/UserDataContext';
|
||||
import { NavLinkGroup, SidebarNav } from './SidebarNav/SidebarNav';
|
||||
import { graphqlToModuleLinks } from '../../utils';
|
||||
import { graphqlToModuleLinks } from '../../utils/utils';
|
||||
import ModuleLayoutContext from '../../context/ModuleLayoutContext';
|
||||
import TableOfContentsSidebar from './TableOfContents/TableOfContentsSidebar';
|
||||
import TableOfContentsBlock from './TableOfContents/TableOfContentsBlock';
|
||||
|
@ -77,7 +77,7 @@ const Breadcrumbs = () => {
|
|||
return (
|
||||
<nav className="flex flex-wrap items-center text-sm leading-loose font-medium">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to="/dashboard/"
|
||||
className="text-gray-500 hover:text-gray-700 transition duration-150 ease-in-out"
|
||||
>
|
||||
Home
|
||||
|
@ -94,7 +94,7 @@ const Breadcrumbs = () => {
|
|||
/>
|
||||
</svg>
|
||||
<Link
|
||||
to={`/${module.section}`}
|
||||
to={`/${module.section}/`}
|
||||
className="text-gray-500 hover:text-gray-700 transition duration-150 ease-in-out"
|
||||
>
|
||||
{SECTION_LABELS[module.section]}
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||
import { ModuleLinkInfo } from '../../../models/module';
|
||||
import ItemLink from './ItemLink';
|
||||
import Accordion from './Accordion';
|
||||
import MODULE_ORDERING, { Category } from '../../../../content/ordering';
|
||||
import MODULE_ORDERING, { Chapter } from '../../../../content/ordering';
|
||||
import { useContext } from 'react';
|
||||
import ModuleLayoutContext from '../../../context/ModuleLayoutContext';
|
||||
|
||||
|
@ -15,7 +15,7 @@ export const SidebarNav = () => {
|
|||
const { module, moduleLinks } = useContext(ModuleLayoutContext);
|
||||
|
||||
const links: NavLinkGroup[] = React.useMemo(() => {
|
||||
return MODULE_ORDERING[module.section].map((category: Category) => ({
|
||||
return MODULE_ORDERING[module.section].map((category: Chapter) => ({
|
||||
label: category.name,
|
||||
children: category.items.map(
|
||||
moduleID => moduleLinks.find(link => link.id === moduleID) // lol O(n^2)?
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import * as React from 'react';
|
||||
// @ts-ignore
|
||||
import logo from '../../assets/logo.svg';
|
||||
import logo from '../assets/logo.svg';
|
||||
// @ts-ignore
|
||||
import logoSquare from '../../assets/logo-square.png';
|
||||
import logoSquare from '../assets/logo-square.png';
|
||||
import { useState } from 'react';
|
||||
import { SECTION_LABELS, SECTIONS } from '../../../content/ordering';
|
||||
import { SECTION_LABELS, SECTIONS } from '../../content/ordering';
|
||||
import { Link } from 'gatsby';
|
||||
|
||||
export default function DashboardNav() {
|
||||
export default function TopNavigationBar() {
|
||||
const [isMobileNavOpen, setIsMobileNavOpen] = useState(false);
|
||||
const links = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
url: '/dashboard',
|
||||
url: '/dashboard/',
|
||||
},
|
||||
...SECTIONS.map(section => ({
|
||||
label: SECTION_LABELS[section],
|
||||
url: `/${section}`,
|
||||
url: `/${section}/`,
|
||||
})),
|
||||
];
|
||||
return (
|
||||
<nav className="bg-white shadow">
|
||||
<nav className="bg-white shadow relative">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-4 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex px-2 lg:px-0">
|
|
@ -1,7 +1,12 @@
|
|||
export class ModuleLinkInfo {
|
||||
public url: string;
|
||||
|
||||
constructor(public id: string, public section: string, public title: string) {
|
||||
constructor(
|
||||
public id: string,
|
||||
public section: string,
|
||||
public title: string,
|
||||
public description?: string
|
||||
) {
|
||||
this.url = `/${section}/${id}`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,32 +12,9 @@ import {
|
|||
moduleIDToURLMap,
|
||||
SECTION_LABELS,
|
||||
} from '../../content/ordering';
|
||||
import DashboardNav from '../components/Dashboard/DashboardNav';
|
||||
import TopNavigationBar from '../components/TopNavigationBar';
|
||||
import ActiveItems, { ActiveItem } from '../components/Dashboard/ActiveItems';
|
||||
|
||||
const getProgressInfo = (
|
||||
keys: string[],
|
||||
data: { [key: string]: string },
|
||||
completedValues: string[],
|
||||
inProgressValues: string[],
|
||||
skippedValues: string[],
|
||||
notStartedValues: string[]
|
||||
) => {
|
||||
let res = {
|
||||
completed: 0,
|
||||
inProgress: 0,
|
||||
skipped: 0,
|
||||
notStarted: 0,
|
||||
};
|
||||
for (let key of keys) {
|
||||
if (!(key in data)) res.notStarted++;
|
||||
else if (completedValues.includes(data[key])) res.completed++;
|
||||
else if (inProgressValues.includes(data[key])) res.inProgress++;
|
||||
else if (skippedValues.includes(data[key])) res.skipped++;
|
||||
else if (notStartedValues.includes(data[key])) res.notStarted++;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
import getProgressInfo from '../utils/getProgressInfo';
|
||||
|
||||
export default function DashboardPage(props: PageProps) {
|
||||
const { modules } = props.data as any;
|
||||
|
@ -128,7 +105,7 @@ export default function DashboardPage(props: PageProps) {
|
|||
<SEO title="Dashboard" />
|
||||
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<DashboardNav />
|
||||
<TopNavigationBar />
|
||||
|
||||
<main className="pb-12">
|
||||
<div className="max-w-7xl mx-auto mb-4">
|
||||
|
|
|
@ -195,7 +195,7 @@ export default function IndexPage(props: PageProps) {
|
|||
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center lg:justify-start">
|
||||
<div className="rounded-md shadow">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to="/dashboard/"
|
||||
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue transition duration-150 ease-in-out md:py-4 md:text-lg md:px-10"
|
||||
>
|
||||
View Guide
|
||||
|
@ -486,7 +486,7 @@ export default function IndexPage(props: PageProps) {
|
|||
<div className="mt-8 flex justify-center">
|
||||
<div className="rounded-md shadow">
|
||||
<Link
|
||||
to="/dashboard"
|
||||
to="/dashboard/"
|
||||
className="w-full flex items-center justify-center px-8 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-500 focus:outline-none focus:border-blue-700 focus:shadow-outline-blue transition duration-150 ease-in-out md:py-4 md:text-lg md:px-10"
|
||||
>
|
||||
View Guide
|
||||
|
|
|
@ -4,7 +4,7 @@ import Layout from '../components/layout';
|
|||
|
||||
import Markdown from '../components/markdown/Markdown';
|
||||
import { SECTION_LABELS } from '../../content/ordering';
|
||||
import { graphqlToModuleInfo } from '../utils';
|
||||
import { graphqlToModuleInfo } from '../utils/utils';
|
||||
import SEO from '../components/seo';
|
||||
import ModuleLayout from '../components/ModuleLayout/ModuleLayout';
|
||||
import { useContext } from 'react';
|
||||
|
|
|
@ -2,34 +2,49 @@ import * as React from 'react';
|
|||
|
||||
import Layout from '../components/layout';
|
||||
import SEO from '../components/seo';
|
||||
import { graphql, Link } from 'gatsby';
|
||||
import { SECTION_LABELS, SECTIONS } from '../../content/ordering';
|
||||
import SyllabusModule from '../components/SyllabusModule';
|
||||
import { getModule } from '../utils';
|
||||
import { graphql } from 'gatsby';
|
||||
import MODULE_ORDERING, {
|
||||
moduleIDToSectionMap,
|
||||
SECTION_LABELS,
|
||||
} from '../../content/ordering';
|
||||
import { getModule } from '../utils/utils';
|
||||
import TopNavigationBar from '../components/TopNavigationBar';
|
||||
import DashboardProgress from '../components/Dashboard/DashboardProgress';
|
||||
import UserDataContext from '../context/UserDataContext';
|
||||
import getProgressInfo from '../utils/getProgressInfo';
|
||||
import ModuleLink from '../components/Dashboard/ModuleLink';
|
||||
import { ModuleLinkInfo } from '../models/module';
|
||||
import styled from 'styled-components';
|
||||
import tw from 'twin.macro';
|
||||
|
||||
const renderModule = (node, idx, parentIdx = -1) => {
|
||||
if (node.hasOwnProperty('items')) {
|
||||
return node.items.map((x, i) => renderModule(x, i, idx));
|
||||
const DottedLineContainer = styled.div`
|
||||
${tw`space-y-6 relative`}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
display: block;
|
||||
left: calc(50% - 1px);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-right: 2px dashed;
|
||||
${tw`border-gray-100`}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const data = node.frontmatter;
|
||||
if (!data.title) return;
|
||||
const SectionContainer = styled.div`
|
||||
${tw`flex flex-col md:flex-row`}
|
||||
|
||||
return (
|
||||
<SyllabusModule
|
||||
title={`${parentIdx !== -1 ? parentIdx + 1 + '.' : ''}${idx + 1}. ${
|
||||
data.title
|
||||
}`}
|
||||
url={node.slug}
|
||||
key={node.slug}
|
||||
problems={data.problems}
|
||||
prerequisites={data.prerequisites}
|
||||
author={data.author}
|
||||
>
|
||||
{data.description}
|
||||
</SyllabusModule>
|
||||
);
|
||||
};
|
||||
&:hover h2 {
|
||||
${tw`text-gray-600`}
|
||||
}
|
||||
&:hover h2 + p {
|
||||
${tw`text-gray-500`}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Template(props) {
|
||||
const data = props.data;
|
||||
|
@ -39,251 +54,110 @@ export default function Template(props) {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
const [selectedDivision, setSelectedDivision] = React.useState(
|
||||
props.pageContext.division
|
||||
const { division } = props.pageContext;
|
||||
|
||||
const section = getModule(allModules, division);
|
||||
|
||||
const { userProgressOnModules, userProgressOnProblems } = React.useContext(
|
||||
UserDataContext
|
||||
);
|
||||
const moduleIDs = section.reduce((acc, cur) => [...acc, ...cur.items], []);
|
||||
let moduleProgressInfo = getProgressInfo(
|
||||
moduleIDs,
|
||||
userProgressOnModules,
|
||||
['Complete'],
|
||||
['Reading', 'Practicing'],
|
||||
['Skipped'],
|
||||
['Not Started']
|
||||
);
|
||||
let problemIDs = [];
|
||||
for (let chapter of MODULE_ORDERING[division]) {
|
||||
for (let moduleID of chapter.items) {
|
||||
for (let problem of allModules[moduleID].problems) {
|
||||
problemIDs.push(problem.uniqueID);
|
||||
}
|
||||
}
|
||||
}
|
||||
const problemsProgressInfo = getProgressInfo(
|
||||
problemIDs,
|
||||
userProgressOnProblems,
|
||||
['Solved'],
|
||||
['Solving'],
|
||||
['Skipped'],
|
||||
['Not Attempted']
|
||||
);
|
||||
const colors = {
|
||||
intro: 'blue',
|
||||
general: 'pink',
|
||||
bronze: 'orange',
|
||||
silver: 'teal',
|
||||
gold: 'yellow',
|
||||
plat: 'purple',
|
||||
adv: 'green',
|
||||
};
|
||||
const color = colors[selectedDivision];
|
||||
const module = getModule(allModules, selectedDivision);
|
||||
|
||||
// for PurgeCSS, we have to list all the classes that are dynamically generated...
|
||||
/*
|
||||
text-blue-400 bg-blue-500 hover:bg-blue-50 hover:text-blue-600 focus:shadow-outline-blue
|
||||
text-pink-400 bg-pink-500 hover:bg-pink-50 hover:text-pink-600 focus:shadow-outline-pink
|
||||
text-orange-400 bg-orange-500 hover:bg-orange-50 hover:text-orange-600 focus:shadow-outline-orange
|
||||
text-teal-400 bg-teal-500 hover:bg-teal-50 hover:text-teal-600 focus:shadow-outline-teal
|
||||
text-yellow-400 bg-yellow-500 hover:bg-yellow-50 hover:text-yellow-600 focus:shadow-outline-yellow
|
||||
text-purple-400 bg-purple-500 hover:bg-purple-50 hover:text-purple-600 focus:shadow-outline-purple
|
||||
text-green-400 bg-green-500 hover:bg-green-50 hover:text-green-600 focus:shadow-outline-green
|
||||
border-blue-500 text-blue-600 focus:text-blue-800 focus:border-blue-700 border-blue-300 focus:border-blue-300 text-blue-500 text-blue-300 bg-blue-600
|
||||
border-pink-500 text-pink-600 focus:text-pink-800 focus:border-pink-700 border-pink-300 focus:border-pink-300 text-pink-500 text-pink-300 bg-pink-600
|
||||
border-orange-500 text-orange-600 focus:text-orange-800 focus:border-orange-700 border-orange-300 focus:border-orange-300 text-orange-500 text-orange-300 bg-orange-600
|
||||
border-teal-500 text-teal-600 focus:text-teal-800 focus:border-teal-700 border-teal-300 focus:border-teal-300 text-teal-500 text-teal-300 bg-teal-600
|
||||
border-yellow-500 text-yellow-600 focus:text-yellow-800 focus:border-yellow-700 border-yellow-300 focus:border-yellow-300 text-yellow-500 text-yellow-300 bg-yellow-600
|
||||
border-purple-500 text-purple-600 focus:text-purple-800 focus:border-purple-700 border-purple-300 focus:border-purple-300 text-purple-500 text-purple-300 bg-purple-600
|
||||
border-green-500 text-green-600 focus:text-green-800 focus:border-green-700 border-green-300 focus:border-green-300 text-green-500 text-green-300 bg-green-600
|
||||
*/
|
||||
// alternatively we can just not dynamically generate classes, but that seems more tedious.
|
||||
|
||||
const selectedTabClasses = `flex-1 py-4 px-3 text-center border-b-2 border-${color}-500 font-bold text-lg leading-5 text-${color}-600 focus:outline-none focus:text-${color}-800 focus:border-${color}-700`,
|
||||
unselectedTabClasses = `flex-1 py-4 px-3 text-center border-b-2 border-transparent font-bold text-lg leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300`;
|
||||
|
||||
const handleDivisionChange = d => {
|
||||
setSelectedDivision(d);
|
||||
window.history.pushState(null, '', `/${d}/`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<SEO title={SECTION_LABELS[selectedDivision]} />
|
||||
<SEO title={SECTION_LABELS[division]} />
|
||||
<div className="min-h-screen">
|
||||
<TopNavigationBar />
|
||||
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Begin Hero Section */}
|
||||
<div
|
||||
className={`relative bg-${color}-600 overflow-hidden transition duration-300 pb-48`}
|
||||
>
|
||||
<div className="hidden sm:block sm:absolute sm:inset-y-0 sm:h-full sm:w-full">
|
||||
<div className="relative h-full max-w-screen-xl mx-auto">
|
||||
<svg
|
||||
className="absolute right-full transform translate-y-1/4 translate-x-1/4 lg:translate-x-1/2"
|
||||
width={404}
|
||||
height={784}
|
||||
fill="none"
|
||||
viewBox="0 0 404 784"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="f210dbf6-a58d-4871-961e-36d5016a0f49"
|
||||
x={0}
|
||||
y={0}
|
||||
width={20}
|
||||
height={20}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={4}
|
||||
height={4}
|
||||
className={`text-${color}-500 transition duration-300`}
|
||||
fill="currentColor"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width={404}
|
||||
height={784}
|
||||
fill="url(#f210dbf6-a58d-4871-961e-36d5016a0f49)"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
className="absolute left-full transform -translate-y-3/4 -translate-x-1/4 md:-translate-y-1/2 lg:-translate-x-1/2"
|
||||
width={404}
|
||||
height={784}
|
||||
fill="none"
|
||||
viewBox="0 0 404 784"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="5d0dd344-b041-4d26-bec4-8d33ea57ec9b"
|
||||
x={0}
|
||||
y={0}
|
||||
width={20}
|
||||
height={20}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={4}
|
||||
height={4}
|
||||
className={`text-${color}-500 transition duration-300`}
|
||||
fill="currentColor"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width={404}
|
||||
height={784}
|
||||
fill="url(#5d0dd344-b041-4d26-bec4-8d33ea57ec9b)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
className={`relative text-center text-xs leading-6 text-${color}-400 pt-2 transition duration-300`}
|
||||
>
|
||||
No part of this website may be reproduced or commercialized in any
|
||||
manner without prior written permission.{' '}
|
||||
<Link to="/license" className="underline">
|
||||
Learn More.
|
||||
</Link>
|
||||
</p>
|
||||
<div className="relative pt-6 pb-12 sm:pb-16 md:pb-20 lg:pb-28 xl:pb-32">
|
||||
<div className="mt-10 mx-auto max-w-screen-xl px-4 sm:mt-12 sm:px-6 md:mt-16 lg:mt-20 xl:mt-28">
|
||||
<div className="text-center">
|
||||
<h2 className="text-4xl tracking-tight leading-10 font-extrabold text-white sm:text-5xl sm:leading-none md:text-6xl">
|
||||
USACO Guide
|
||||
</h2>
|
||||
<p
|
||||
className={`mt-3 max-w-md mx-auto text-base text-${color}-300 sm:text-lg md:mt-5 md:text-xl md:max-w-3xl transition duration-300`}
|
||||
>
|
||||
A collection of curated, high-quality resources to take you
|
||||
from Bronze to Platinum.
|
||||
</p>
|
||||
<div className="mt-5 max-w-md mx-auto sm:flex sm:justify-center md:mt-8">
|
||||
<div className="rounded-md shadow">
|
||||
<Link
|
||||
to="/"
|
||||
className={`w-full flex items-center justify-center px-8 py-3 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-${color}-500 hover:bg-${color}-50 hover:text-${color}-600 focus:outline-none focus:shadow-outline-${color} transition duration-300 ease-in-out md:py-4 md:text-lg md:px-10`}
|
||||
>
|
||||
About This Guide
|
||||
</Link>
|
||||
<main>
|
||||
<div className="bg-gray-100 py-12">
|
||||
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<h1 className="mb-8 text-blue-700 text-5xl tracking-tight leading-10 font-extrabold text-white sm:leading-none md:text-6xl text-center">
|
||||
{SECTION_LABELS[division]}
|
||||
</h1>
|
||||
<div className="grid max-w-2xl mx-auto lg:max-w-full lg:grid-cols-2 gap-8">
|
||||
<div className="bg-white shadow sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Modules Progress
|
||||
</h3>
|
||||
<div className="mt-6">
|
||||
<DashboardProgress
|
||||
{...moduleProgressInfo}
|
||||
total={moduleIDs.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white shadow sm:rounded-lg">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Problems Progress
|
||||
</h3>
|
||||
<div className="mt-6">
|
||||
<DashboardProgress
|
||||
{...problemsProgressInfo}
|
||||
total={section.length}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* End Hero Section */}
|
||||
|
||||
<div className="pb-8" id="content">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 pb-4">
|
||||
<div
|
||||
className="mb-8 bg-white shadow-md rounded-lg relative"
|
||||
style={{ marginTop: '-12rem' }}
|
||||
>
|
||||
<div className="sm:hidden">
|
||||
<select
|
||||
aria-label="Selected tab"
|
||||
className="form-select block w-full"
|
||||
onChange={e => handleDivisionChange(e.target.value)}
|
||||
value={selectedDivision}
|
||||
>
|
||||
{SECTIONS.map(division => (
|
||||
<option key={division} value={division}>
|
||||
{SECTION_LABELS[division]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="hidden sm:block border-b border-gray-200">
|
||||
<nav className="flex -mb-px">
|
||||
{SECTIONS.map(division => (
|
||||
<a
|
||||
key={division}
|
||||
href={`/${division}`}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
handleDivisionChange(division);
|
||||
}}
|
||||
className={
|
||||
selectedDivision === division
|
||||
? selectedTabClasses
|
||||
: unselectedTabClasses
|
||||
<DottedLineContainer className="py-12 px-4">
|
||||
{section.map(category => (
|
||||
<SectionContainer>
|
||||
<div className="flex-1 md:text-right pr-12 group">
|
||||
<h2 className="text-2xl font-semibold leading-6 py-3 text-gray-500 group-hover:text-gray-800 transition duration-150 ease-in-out">
|
||||
{category.name}
|
||||
</h2>
|
||||
<p className="md:max-w-sm md:ml-auto text-gray-400 group-hover:text-gray-600 transition duration-150 ease-in-out">
|
||||
{category.description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 pl-12">
|
||||
{category.items.map(item => (
|
||||
<ModuleLink
|
||||
link={
|
||||
new ModuleLinkInfo(
|
||||
item.frontmatter.id,
|
||||
moduleIDToSectionMap[item.frontmatter.id],
|
||||
item.frontmatter.title,
|
||||
item.frontmatter.description
|
||||
)
|
||||
}
|
||||
>
|
||||
{SECTION_LABELS[division]}
|
||||
</a>
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<ol className="list-inside p-4 sm:p-8 text-lg space-y-1">
|
||||
{module.map((m, idx) => {
|
||||
if (m.hasOwnProperty('items')) {
|
||||
return (
|
||||
<li key={m.name}>
|
||||
<span className="inline-block w-4 text-right">
|
||||
{idx + 1}.{' '}
|
||||
</span>
|
||||
<span className="ml-2">{m.name}</span>
|
||||
<ol className="list-inside px-6 text-lg space-y-1 mb-2">
|
||||
{m.items.map((m, idx2) => (
|
||||
<li key={m.slug}>
|
||||
<span className="inline-block w-8 text-right">
|
||||
{idx + 1}.{idx2 + 1}.{' '}
|
||||
</span>
|
||||
<Link
|
||||
className="ml-2 text-blue-600 underline"
|
||||
to={m.slug}
|
||||
>
|
||||
{m.frontmatter.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<li key={m.frontmatter.id}>
|
||||
<span className="inline-block w-4 text-right">
|
||||
{idx + 1}.{' '}
|
||||
</span>
|
||||
<Link
|
||||
className="ml-2 text-blue-600 underline"
|
||||
to={m.slug}
|
||||
>
|
||||
{m.frontmatter.title}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{module.map((x, idx) => renderModule(x, idx))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SectionContainer>
|
||||
))}
|
||||
</DottedLineContainer>
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
|
@ -301,6 +175,9 @@ export const pageQuery = graphql`
|
|||
prerequisites
|
||||
description
|
||||
}
|
||||
problems {
|
||||
uniqueID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
src/utils/getProgressInfo.ts
Normal file
25
src/utils/getProgressInfo.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
const getProgressInfo = (
|
||||
keys: string[],
|
||||
data: { [key: string]: string },
|
||||
completedValues: string[],
|
||||
inProgressValues: string[],
|
||||
skippedValues: string[],
|
||||
notStartedValues: string[]
|
||||
) => {
|
||||
let res = {
|
||||
completed: 0,
|
||||
inProgress: 0,
|
||||
skipped: 0,
|
||||
notStarted: 0,
|
||||
};
|
||||
for (let key of keys) {
|
||||
if (!(key in data)) res.notStarted++;
|
||||
else if (completedValues.includes(data[key])) res.completed++;
|
||||
else if (inProgressValues.includes(data[key])) res.inProgress++;
|
||||
else if (skippedValues.includes(data[key])) res.skipped++;
|
||||
else if (notStartedValues.includes(data[key])) res.notStarted++;
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
export default getProgressInfo;
|
|
@ -1,5 +1,5 @@
|
|||
import MODULE_ORDERING from '../content/ordering';
|
||||
import { ModuleInfo, ModuleLinkInfo } from './models/module';
|
||||
import MODULE_ORDERING from '../../content/ordering';
|
||||
import { ModuleInfo, ModuleLinkInfo } from '../models/module';
|
||||
|
||||
export const getModule = (allModules, division) => {
|
||||
return MODULE_ORDERING[division].map(k => {
|
||||
|
@ -16,6 +16,7 @@ export const getModule = (allModules, division) => {
|
|||
slug: `/${division}/${allModules[k2 as string].frontmatter.id}`,
|
||||
};
|
||||
}),
|
||||
description: k.description,
|
||||
};
|
||||
} else {
|
||||
if (!allModules.hasOwnProperty(k)) {
|
Reference in a new issue