Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions ops/scaffold-pages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import json

default_file_content = '''\
---
title: {title}
layout: '@/layouts/learn.astro'
---

TODO: add content
'''


def get_json_contents(filename):
with open(f'src/navData/{filename}.json') as f:
return json.load(f)


def create_doc(route):
filepath = f"src/pages/{route.get('path')}.md"
file_content = default_file_content.format(title=route.get('title'))
with open(filepath, 'w+') as file:
file.write(file_content)


def iterate_toc(toc):
for route in toc.get('routes'):
if bool(route.get('routes')):
iterate_toc(route)
create_doc(route)


filename = input('Source File: ')
toc = get_json_contents(filename)
iterate_toc(toc)
1 change: 1 addition & 0 deletions src/components/Header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Logo from '@/components/Logo.astro';
<div class="ml-auto flex items-end space-x-4">
<HeaderLink href="/#about">About</HeaderLink>
<HeaderLink href="/blog">Blog</HeaderLink>
<HeaderLink href="/learn">Learn</HeaderLink>
<span
class="block border-b border-transparent py-1 text-base uppercase leading-6 text-gray-500 hover:border-blue-600 hover:text-blue-600 hover:no-underline focus:text-blue-600 dark:text-blue-100 dark:hover:border-sky-400 dark:hover:text-sky-400 dark:focus:text-sky-400"
>
Expand Down
19 changes: 19 additions & 0 deletions src/components/Icons/CaretRightIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function CaretRightIcon({ className = '' }) {
return (
<svg
className={className}
width="1em"
height="1em"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.86612 13.6161C6.37796 14.1043 6.37796 14.8957 6.86612 15.3839C7.35427 15.872 8.14572 15.872 8.63388 15.3839L13.1339 10.8839C13.622 10.3957 13.622 9.60428 13.1339 9.11612L8.63388 4.61612C8.14572 4.12797 7.35427 4.12797 6.86612 4.61612C6.37796 5.10428 6.37796 5.89573 6.86612 6.38388L10.4822 10L6.86612 13.6161Z"
fill="currentColor"
/>
</svg>
);
}

export default CaretRightIcon;
17 changes: 17 additions & 0 deletions src/components/Icons/CloseIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function CloseIcon({ className = '' }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="currentColor"
>
<path fill="none" d="M0 0h24v24H0z" />
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>
);
}

export default CloseIcon;
35 changes: 35 additions & 0 deletions src/components/Icons/IconNavArrow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import cn from 'classnames';

function IconNavArrow({ displayDirection = 'right', className }) {
const classes = cn(
'duration-100 ease-in transition',
{
'rotate-0': displayDirection === 'down',
'-rotate-90': displayDirection === 'right',
'rotate-90': displayDirection === 'left',
},
className
);

return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 20 20"
className={classes}
>
<g fill="none" fillRule="evenodd" transform="translate(-446 -398)">
<path
fill="currentColor"
fillRule="nonzero"
d="M95.8838835,240.366117 C95.3957281,239.877961 94.6042719,239.877961 94.1161165,240.366117 C93.6279612,240.854272 93.6279612,241.645728 94.1161165,242.133883 L98.6161165,246.633883 C99.1042719,247.122039 99.8957281,247.122039 100.383883,246.633883 L104.883883,242.133883 C105.372039,241.645728 105.372039,240.854272 104.883883,240.366117 C104.395728,239.877961 103.604272,239.877961 103.116117,240.366117 L99.5,243.982233 L95.8838835,240.366117 Z"
transform="translate(356.5 164.5)"
/>
<polygon points="446 418 466 418 466 398 446 398" />
</g>
</svg>
);
}

export default IconNavArrow;
17 changes: 17 additions & 0 deletions src/components/Icons/MenuIcon.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
function MenuIcon({ className = '' }) {
return (
<svg
className={className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="1em"
height="1em"
fill="currentColor"
>
<path fill="none" d="M0 0h24v24H0z" />
<path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" />
</svg>
);
}

export default MenuIcon;
4 changes: 4 additions & 0 deletions src/components/Icons/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
export { default as CaretRightIcon } from './CaretRightIcon';
export { default as CloseIcon } from './CloseIcon';
export { default as CodeIcon } from './CodeIcon';
export { default as DevToIcon } from './DevToIcon';
export { default as GithubIcon } from './GithubIcon';
export { default as IconNavArrow } from './IconNavArrow';
export { default as MenuIcon } from './MenuIcon';
export { default as MicIcon } from './MicIcon';
export { default as MoonIcon } from './MoonIcon';
export { default as PencilIcon } from './PencilIcon';
Expand Down
35 changes: 35 additions & 0 deletions src/components/Learn/Breadcrumbs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
import { RouteItem } from '@/utils/nav';
import { CaretRightIcon } from '@/components/Icons';

export interface Props {
breadcrumbs: RouteItem[];
}

const { breadcrumbs } = Astro.props as Props;
---

<div class="flex">
{
breadcrumbs.map((crumb) => {
if (!crumb.path) {
return null;
}
return (
<div class="flex items-center">
<>
<a
href={crumb.path}
class="mr-1 text-sm uppercase tracking-wide hover:underline"
>
{crumb.title}
</a>
<span class="mr-1 inline-block text-lg">
<CaretRightIcon />
</span>
</>
</div>
);
})
}
</div>
34 changes: 34 additions & 0 deletions src/components/Learn/Nav.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
import { RouteItem } from '@/utils/nav';

export interface Props {
navContent: RouteItem;
}

const { navContent } = Astro.props as Props;
const navPaths = navContent.routes;
---

<nav
class="w-full h-full max-h-full overflow-x-visible overflow-y-auto"
aria-label="Learn Navigation"
>
<ul class="py-4">
{
navPaths.map(({ path, title, routes }) => {
return (
<>
<a href={path}>{title}</a>
<ul>
{routes?.map(({ title: subRouteTitle, path: subRoutePath }) => (
<li>
<a href={subRoutePath}>{subRouteTitle}</a>
</li>
))}
</ul>
</>
);
})
}
</ul>
</nav>
36 changes: 36 additions & 0 deletions src/components/Learn/Pagination.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
import { RouteMeta } from '@/utils/nav';
import PaginationLink from './PaginationLink.astro';

export type Props = Pick<RouteMeta, 'nextRoute' | 'prevRoute'>;

const { nextRoute, prevRoute } = Astro.props as Props;
---

{
(prevRoute?.path || nextRoute?.path) && (
<>
<div class="mx-auto grid grid-cols-1 gap-4 py-4 sm:grid-cols-2 md:py-12">
{prevRoute?.path ? (
<PaginationLink
type="Previous"
title={prevRoute.title}
href={prevRoute.path}
/>
) : (
<div />
)}

{nextRoute?.path ? (
<PaginationLink
type="Next"
title={nextRoute.title}
href={nextRoute.path}
/>
) : (
<div />
)}
</div>
</>
)
}
33 changes: 33 additions & 0 deletions src/components/Learn/PaginationLink.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
import cn from 'classnames';
import { IconNavArrow } from '@/components/Icons';

export interface Props {
href: string;
title: string;
type: 'Previous' | 'Next';
}

const { href, title, type } = Astro.props as Props;
---

<a
href={href}
class={cn(
'flex gap-x-4 md:gap-x-6 items-center w-full md:w-80 px-4 md:px-5 py-6 border-2 border-transparent text-base leading-base text-link dark:text-link-dark rounded-lg group focus:text-link dark:focus:text-link-dark focus:bg-highlight focus:border-link dark:focus:bg-highlight-dark dark:focus:border-link-dark focus:border-opacity-100 focus:border-2 focus:ring-1 focus:ring-offset-4 focus:ring-blue-40 active:ring-0 active:ring-offset-0 hover:bg-gray-5 dark:hover:bg-gray-80',
{ 'flex-row-reverse justify-self-end text-right': type === 'Next' }
)}
>
<IconNavArrow
className="text-gray-30 dark:text-gray-50 inline group-focus:text-link dark:group-focus:text-link-dark"
displayDirection={type === 'Previous' ? 'left' : 'right'}
/>
<span>
<span
class="block no-underline text-sm tracking-wide text-secondary dark:text-secondary-dark uppercase font-bold group-focus:text-link dark:group-focus:text-link-dark group-focus:text-opacity-100"
>
{type}
</span>
<span class="block text-lg group-hover:underline">{title}</span>
</span>
</a>
99 changes: 99 additions & 0 deletions src/components/Learn/TableOfContents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { FunctionalComponent } from 'preact';
import { useState, useEffect, useRef } from 'preact/hooks';
import cn from 'classnames';
import { unescapeHtml } from '@/utils/unescapeHtml';

interface Props {
headers: { depth: number; slug: string; text: string }[];
}

const TableOfContents: FunctionalComponent<Props> = ({ headers = [] }) => {
const targetedHeaders = [...headers].filter(
({ depth }) => depth > 1 && depth < 4
);
targetedHeaders.unshift({
depth: 1,
text: 'Overview',
slug: '',
});
const toc = useRef<HTMLUListElement>();
const [currentID, setCurrentID] = useState(targetedHeaders?.[0]?.slug);
const onThisPageID = 'on-this-page-heading';

useEffect(() => {
if (!toc.current) {
return;
}

const setCurrent: IntersectionObserverCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const { id } = entry.target;
if (id === onThisPageID) {
continue;
}
setCurrentID(entry.target.id);
break;
}
}
};

const observerOptions: IntersectionObserverInit = {
// Negative top margin accounts for `scroll-margin`.
// Negative bottom margin means heading needs to be towards top of viewport to trigger intersection.
rootMargin: '-90px 0% -66%',
threshold: 1,
};

const headingsObserver = new IntersectionObserver(
setCurrent,
observerOptions
);

// Observe all the headings in the main page content.
document
.querySelectorAll('main :is(h1,h2,h3)')
.forEach((h) => headingsObserver.observe(h));

// Stop observing when the component is unmounted.
return () => headingsObserver.disconnect();
}, [toc.current]);

const onLinkClick = (e) => {
setCurrentID(e.target.getAttribute('href').replace('#', ''));
};

return (
<>
<h2
class="text-secondary dark:text-secondary-dark mb-3 w-full px-4 text-sm font-bold uppercase tracking-wide lg:mb-3"
id={onThisPageID}
>
On This Page
</h2>
<ul
class="m-0 h-full list-none space-y-1 overflow-y-auto pb-16 pl-4"
ref={toc}
>
{targetedHeaders.map(({ depth, slug, text }) => (
<li
class={cn('m-0 rounded-l-lg px-2 text-sm', {
'dark:bg-highlight-dark bg-blue-300': currentID === slug,
'pl-8': depth === 3,
})}
>
<a
href={`#${slug}`}
onClick={onLinkClick}
class="text-link dark:text-link-dark hover:text-link dark:hover:text-link-dark block py-2 font-semibold leading-normal"
>
{unescapeHtml(text)}
</a>
</li>
))}
</ul>
</>
);
};

export default TableOfContents;
2 changes: 1 addition & 1 deletion src/layouts/base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const { title, description, socialImage, canonicalURL, redirectUrl } =
<Head {title} {description} {socialImage} {canonicalURL} {redirectUrl} />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Montserrat&display=swap"
href="https://fonts.googleapis.com/css?family=Montserrat:100,200,300,400,500,600,700,800,900&display=swap"
/>
<link
rel="stylesheet"
Expand Down
Loading