on master help sign in
Merge pull request #668 from urbit/urbit.org2-docs-ga

Docs routing and sidebar

commitaa9f8abffc30b965fa97012be2622c1be53c6522 authorGavin <5265021+g-a-v-i-n@users.noreply.github.com> dateFri 25 Jun 2021, 16:40 (5 years ago) parentab7dbab19792d0
17 files changed+502−320
components/ContactSection.js+38 components/ContentArea.js+24 components/Header.js+1−1 components/Sidebar.js+17 content/blog/gifts-q3-2020.md+1 content/blog/urbit-grants-and-mid-2019-gifts.md+1 content/updates/2018-2-17-update.md+1 lib/lib.js+47−25 pages/blog.js+1−1 pages/docs.js−141 pages/docs/[...slug].js → pages/docs/[[...slug]].js+43−61 pages/faq.js+86 pages/grants.js+66−76 pages/grants/[slug].js+146 pages/index.js+6−8 styles/globals.css+10−7 tailwind.config.js+14
components/ContactSection.jsnew file+38
@@ -0,0 +1,38 @@
1+import NewsletterSignup from "../../components/NewletterSignup";
2+import { name, contact } from "../../lib/constants";
3+
4+export default function () {
5+ return (
6+ <section className="layout-narrow">
7+ <div className="measure">
8+ <h4 className="pb-6">
9+ If you’d like to follow our progress, we send monthly updates via
10+ email:
11+ </h4>
12+ </div>
13+ <NewsletterSignup color="black" />
14+ <h4 className="mt-12 text-gray">
15+ Follow us on{" "}
16+ <a className="text-black" href={contact.twitter}>
17+ Twitter
18+ </a>
19+ </h4>
20+ <h4 className="mt-6 text-gray">
21+ Explore code on{" "}
22+ <a className="text-black" href={contact.github}>
23+ Github
24+ </a>
25+ </h4>
26+ <h4 className="mt-6 text-gray">
27+ Ask questions in our{" "}
28+ <a className="text-black" href={contact.discord}>
29+ Discord
30+ </a>
31+ </h4>
32+ <h4 className="mt-6 text-gray">
33+ Boot Urbit and join{" "}
34+ <code className="bg-wall p-2 rounded-lg">{contact.urbitCommunity}</code>
35+ </h4>
36+ </section>
37+ );
38+}
components/ContentArea.jsnew file+24
@@ -0,0 +1,24 @@
1+export default function ContentArea(props) {
2+ return (
3+ <div className="w-full">
4+ <header className="flex justify-between items-center px-24 pt-12 pb-8">
5+ <div className="type-ui">Urbit Documentation</div>
6+ <button
7+ onClick={(e) => {
8+ e.stopPropagation();
9+ props.toggleSearch();
10+ }}
11+ className="button-sm bg-wall text-gray"
12+ >
13+ Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
14+ </button>
15+ </header>
16+ <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
17+ <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
18+ <h2 className="mb-16 mt-4">{props.title}</h2>
19+ {props.children}
20+ <div className="pb-32" />
21+ </div>
22+ </div>
23+ );
24+}
components/Header.js+1−1
@@ -50,7 +50,7 @@ function MenuTray(props) {
5050export default function Header(props) {
5151 const [isOpen, toggleTray] = useState(false);
5252 return (
53− <header className="layout-wide flex justify-between items-center pt-8">
53+ <header className="layout-wide flex justify-between items-center pt-12">
5454 <div className="absolute z-10">
5555 <Link href="/">
5656 <a className="type-ui">Urbit</a>
components/Sidebar.jsnew file+17
@@ -0,0 +1,17 @@
1+import Link from "next/link";
2+
3+export default function Sidebar(props) {
4+ return (
5+ <div className="flex flex-col w-96 bg-wall max-h-screen h-screen">
6+ <header className="flex flex-shrink-0 justify-between items-center pl-6 pt-12 pb-8">
7+ <Link href="/">
8+ <a className="type-ui text-gray">Urbit.org</a>
9+ </Link>
10+ </header>
11+ <div className="overflow-y-scroll p-6 pt-16">
12+ {props.children}
13+ <div className="pb-32" />
14+ </div>
15+ </div>
16+ );
17+}
content/blog/gifts-q3-2020.md+1
@@ -4,6 +4,7 @@ date = 2020-09-23
44description = "Twice a year we distribute address space to those that have made valuable contributions to Urbit. Now called our Gifts program, the gifting of address space has been part of Urbit long before we had a grants program."
55[taxonomies]
66grant_type = ["Gift"]
7+grant_category = []
78[extra]
89author = "Josh Lehman"
910ship = "~wolref-podlex"
content/blog/urbit-grants-and-mid-2019-gifts.md+1
@@ -5,6 +5,7 @@ description = "Announcing an upcoming Urbit grants program and star gifts for Mi
55aliases = ["/posts/essays/urbit-grants-and-mid-2019-gifts", "/posts/urbit-grants-and-mid-2019-gifts"]
66[taxonomies]
77grant_type = ["Gift"]
8+grant_category = []
89[extra]
910author = "Galen Wolfe-Pauly + Alex Matzner"
1011ship = "~ravmel-ropdyl + ~mignyt-mogseb"
content/updates/2018-2-17-update.md+1
@@ -5,6 +5,7 @@ description = "A short update from the Tlon team for the week of ~2018.2.17."
55aliases = ["/posts/2018-2-17-update/"]
66[taxonomies]
77grant_type = ["Gift"]
8+grant_category = []
89[extra]
910ship = "~tonlur-sarret"
1011author = ""
lib/lib.js+47−25
@@ -5,11 +5,6 @@ import { join, parse } from "path";
55import matter from "gray-matter";
66import toml from "@iarna/toml";
77
8−// export async function markdownToHtml(markdown) {
9−// const result = await remark().use(html).process(markdown)
10−// return result.toString()
11−// }
12−
138const directories = {
149 blog: join(process.cwd(), "content/blog"),
1510 events: join(process.cwd(), "content/events"),
@@ -69,6 +64,26 @@ export function getAllPosts(fields = [], key) {
6964 return posts;
7065}
7166
67+function getAllPaths(dirPath, arrayOfFiles) {
68+ let files = fs.readdirSync(dirPath);
69+
70+ arrayOfFiles = arrayOfFiles || [];
71+
72+ files.forEach(function (file) {
73+ if (fs.statSync(dirPath + "/" + file).isDirectory()) {
74+ arrayOfFiles = getAllPaths(dirPath + "/" + file, arrayOfFiles);
75+ } else {
76+ arrayOfFiles.push(join(dirPath, "/", file));
77+ }
78+ });
79+
80+ return arrayOfFiles;
81+}
82+
83+export function getDocsPaths() {
84+ return getAllPaths(directories.docs, []);
85+}
86+
7287export function getNextPost(slug, fields = [], key) {
7388 let resultPost = null;
7489
@@ -144,35 +159,42 @@ export function formatDate(d) {
144159// children: { [string]: PageTree },
145160// }
146161
147−export function buildPageTree(path, type = "", content = false) {
162+export function buildPageTree(path, ordering = "", content = false) {
163+ // get metadata from _index.md file in each folder. This is where page ordering (weight) is stored.
148164 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
149−
165+ // get a list of contents at the path, with file types so that the item at the path can be identified as file or folder
150166 const children = fs.readdirSync(path, { withFileTypes: true });
167+ // get a list of files
151168 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
169+ // get a list of folders
152170 const subdirs = children.filter((f) => f.isDirectory());
153−
171+ // return a pagetree datastructure
154172 return {
155173 title: metadata.data.title,
156− pages: pages.map((page) => {
157− const { data: pageData, content: pageContent } = matter(
158− fs.readFileSync(join(path, page.name)),
159− options
160− );
161− return {
162− title: pageData.title,
163− base: page.name,
164− slug: page.name.replace(/.md$/, ""),
165− ...(type === "weight" && { weight: pageData?.weight ?? 0 }),
166− ...(type === "date" && {
167− date: pageData?.date ?? "2000-01-01T00:00:00.000Z",
168− }),
169− ...(content && { content: pageContent }),
170− };
171− }),
174+ pages: pages
175+ .map((page) => {
176+ // retrieve topmatter as JSON
177+ const { content, data } = matter(
178+ fs.readFileSync(join(path, page.name)),
179+ options
180+ );
181+
182+ return {
183+ title: data.title,
184+ base: page.name,
185+ slug: page.name.replace(/.md$/, ""),
186+ ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
187+ ...(ordering === "date" && {
188+ date: data?.date ?? "2000-01-01T00:00:00.000Z",
189+ }),
190+ };
191+ })
192+ // Sort files according to weight. Weight can be a date or number. The choice between the two is set by the 'ordering' param.
193+ .sort((a, b) => a.weight - b.weight),
172194 children: Object.fromEntries(
173195 subdirs.map((subdir) => [
174196 subdir.name,
175− buildPageTree(join(path, subdir.name), type, content),
197+ buildPageTree(join(path, subdir.name), ordering, content),
176198 ])
177199 ),
178200 };
pages/blog.js+1−1
@@ -18,7 +18,7 @@ export default function Blog({ posts }) {
1818 <Header toggleSearch={() => toggleSearch(true)} />
1919 <section className="layout-wide">
2020 <div className="measure">
21− <h2 className="pb-16">Urbit Blog</h2>
21+ <h2 className="pb-16">FAQ</h2>
2222 <p className="pb-6">
2323 Stories from the broader Urbit community, the Urbit Foundation,
2424 and the many people contributing to Urbit.
pages/docs.jsdeleted−141
@@ -1,141 +0,0 @@
1−import Head from "next/head";
2−import Link from "next/link";
3−import { useState } from "react";
4−import classnames from "classnames";
5−import { join } from "path";
6−import { buildPageTree, getPage } from "../lib/lib";
7−import Markdown from "../components/Markdown";
8−import Search from "../components/Search";
9−import { configure, GlobalHotKeys } from "react-hotkeys";
10−
11−const breadcrumbs = (posts, paths) => {
12− const results = [
13− <Link href="/">Urbit.org</Link>,
14− <span className="px-1">/</span>,
15− <Link href="/docs">Documentation</Link>,
16− ];
17− let thisLink = "/docs";
18− for (const path of paths) {
19− posts = posts.children[path];
20− thisLink = join(thisLink, path);
21− results.push(
22− <span className="px-1">/</span>,
23− <Link href={thisLink}>{posts.title}</Link>
24− );
25− }
26− return results;
27−};
28−
29−const childPages = (thisLink, children, level = 0) => (
30− <ul className="pl-1">
31− {Object.entries(children).map(([childSlug, child]) => (
32− <li>{pageTree(join(thisLink, childSlug), child, level)}</li>
33− ))}
34− </ul>
35−);
36−
37−const pageTree = (thisLink, tree, level = 0) => {
38− const [isOpen, toggleTree] = useState(false);
39−
40− const activeClasses = classnames({
41− hidden: !isOpen,
42− });
43− return (
44− <>
45− <span
46− className={
47− "font-medium text-gray text-lg cursor-pointer hover:text-black"
48− }
49− onClick={() => toggleTree(!isOpen)}
50− >
51− {tree.title}
52− </span>
53− <div className={activeClasses}>
54− <ul className={"pl-1"}>
55− {tree.pages
56− .sort((a, b) => a.weight - b.weight)
57− .map(({ title, slug }) => (
58− <li className="font-thin py-1">
59− <Link href={join(thisLink, slug)}>{title}</Link>
60− </li>
61− ))}
62− </ul>
63− {childPages(thisLink, tree.children, level + 1)}
64− </div>
65− </>
66− );
67−};
68−
69−function Sidebar(props) {
70− return (
71− <div className="flex flex-col w-72 bg-wall max-h-screen h-screen">
72− <header className="flex flex-shrink-0 justify-between items-center pl-12 pt-12 pb-8">
73− <Link href="/">
74− <a className="type-ui text-gray">Urbit.org</a>
75− </Link>
76− </header>
77− <div className="overflow-y-scroll p-12 pt-16">
78− {props.children}
79− <div className="pb-32" />
80− </div>
81− </div>
82− );
83−}
84−
85−function ContentArea(props) {
86− return (
87− <div className="w-full">
88− <header className="flex justify-between items-center px-24 pt-12 pb-8">
89− <div className="type-ui">Urbit Documentation</div>
90− <button
91− onClick={(e) => {
92− e.stopPropagation();
93− props.toggleSearch();
94− }}
95− className="button-sm bg-wall text-gray"
96− >
97− Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
98− </button>
99− </header>
100− <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
101− <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
102− <h2 className="mb-16 mt-4">{props.title}</h2>
103− {props.children}
104− <div className="pb-32" />
105− </div>
106− </div>
107− );
108−}
109−
110−export default function DocsLayout({ posts, data, content }) {
111− const [showSearch, toggleSearch] = useState(false);
112− const toggle = () => toggleSearch((state) => !state);
113− const keyMap = { search: ["command+k", "ctrl+k", "esc"] };
114− const handlers = { search: toggle };
115− configure({
116− ignoreTags: [],
117− });
118− return (
119− <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
120− <GlobalHotKeys keyMap={keyMap} handlers={handlers} />
121− <Head>
122− <title>Documentation / Urbit.org</title>
123− </Head>
124− {showSearch && <Search toggleSearch={() => toggleSearch(false)} />}
125− <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
126− <ContentArea
127− breadcrumbs={breadcrumbs(posts, [])}
128− title={data.title}
129− toggleSearch={() => toggleSearch(true)}
130− >
131− <Markdown post={{ content: content }} />
132− </ContentArea>
133− </div>
134− );
135−}
136−
137−export async function getStaticProps() {
138− const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
139− const { data, content } = getPage(join(process.cwd(), "content/docs"));
140− return { props: { posts, data, content } };
141−}
pages/docs/[...slug].js → pages/docs/[[...slug]].jsrenamed+43−61
@@ -6,6 +6,8 @@ import classnames from "classnames";
66import { join } from "path";
77import { getDocs, formatDate, buildPageTree, getPage } from "../../lib/lib";
88import Markdown from "../../components/Markdown";
9+import ContentArea from "../../components/ContentArea";
10+import Sidebar from "../../components/Sidebar";
911import Search from "../../components/Search";
1012import { configure, GlobalHotKeys } from "react-hotkeys";
1113
@@ -40,37 +42,53 @@ const pageTree = (thisLink, tree, level = 0) => {
4042 const firstCrumb = "/" + router.asPath.split("/").slice(1, -1).join("/");
4143
4244 const includesThisPage = firstCrumb.includes(thisLink);
43− const isThisPage = firstCrumb === thisLink;
45+ const isThisPage = router.asPath === thisLink;
4446 const [isOpen, toggleTree] = useState(includesThisPage);
4547
4648 const activeClasses = classnames({
4749 hidden: !isOpen,
4850 });
4951
50− const headerClass = classnames({
51− "text-gray": !isThisPage,
52+ const headingItemClasses = classnames({
53+ "pl-0 text-black text-base font-semibold hover:text-green leading-relaxed":
54+ level === 0,
55+ "pl-4 text-black text-base font-semibold hover:text-green": level === 1,
56+ "pl-8 text-black text-base hover:text-green": level === 2,
57+ });
58+
59+ const pageItemClasses = classnames({
60+ "pl-4 text-black text-base hover:text-green": level === 0,
61+ "pl-8 text-black text-base hover:text-green": level === 1,
62+ "pl-12 text-black text-base hover:text-green": level === 2,
5263 dot: isThisPage,
5364 });
65+
5466 return (
5567 <>
56− <span
57− className={
58− "font-medium text-lg cursor-pointer hover:text-black relative " +
59− headerClass
60− }
61− onClick={() => toggleTree(!isOpen)}
62− >
63− {tree.title}
68+ <span onClick={() => toggleTree(!isOpen)}>
69+ <p className={`${headingItemClasses} cursor-pointer`}>{tree.title}</p>
6470 </span>
6571 <div className={activeClasses}>
6672 <ul className={"pl-1"}>
67− {tree.pages
68− .sort((a, b) => a.weight - b.weight)
69− .map(({ title, slug }) => (
70− <li className="font-thin py-1">
71− <Link href={join(thisLink, slug)}>{title}</Link>
73+ {tree.pages.map(({ title, slug }) => {
74+ const href = join(thisLink, slug);
75+ const isSelected = router.asPath === href;
76+ const selectedClasses = classnames({
77+ dot: isSelected,
78+ "text-green": isSelected,
79+ });
80+ return (
81+ <li>
82+ <Link href={href}>
83+ <a
84+ className={`relative ${pageItemClasses} ${selectedClasses}`}
85+ >
86+ {title}
87+ </a>
88+ </Link>
7289 </li>
73− ))}
90+ );
91+ })}
7492 </ul>
7593 {childPages(thisLink, tree.children, level + 1)}
7694 </div>
@@ -78,47 +96,6 @@ const pageTree = (thisLink, tree, level = 0) => {
7896 );
7997};
8098
81−function Sidebar(props) {
82− return (
83− <div className="flex flex-col w-72 bg-wall max-h-screen h-screen">
84− <header className="flex flex-shrink-0 justify-between items-center pl-12 pt-12 pb-8">
85− <Link href="/">
86− <a className="type-ui text-gray">Urbit.org</a>
87− </Link>
88− </header>
89− <div className="overflow-y-scroll p-12 pt-16">
90− {props.children}
91− <div className="pb-32" />
92− </div>
93− </div>
94− );
95−}
96−
97−function ContentArea(props) {
98− return (
99− <div className="w-full">
100− <header className="flex justify-between items-center px-24 pt-12 pb-8">
101− <div className="type-ui">Urbit Documentation</div>
102− <button
103− onClick={(e) => {
104− e.stopPropagation();
105− props.toggleSearch();
106− }}
107− className="button-sm bg-wall text-gray"
108− >
109− Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
110− </button>
111− </header>
112− <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
113− <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
114− <h2 className="mb-16 mt-4">{props.title}</h2>
115− {props.children}
116− <div className="pb-32" />
117− </div>
118− </div>
119− );
120−}
121−
12299export default function DocsLayout({ posts, data, content, params }) {
123100 const [showSearch, toggleSearch] = useState(false);
124101 const toggle = () => toggleSearch((state) => !state);
@@ -137,7 +114,7 @@ export default function DocsLayout({ posts, data, content, params }) {
137114 <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
138115 <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
139116 <ContentArea
140− breadcrumbs={breadcrumbs(posts, params.slug.slice(0, -1))}
117+ breadcrumbs={breadcrumbs(posts, params.slug?.slice(0, -1) || "")}
141118 title={data.title}
142119 toggleSearch={() => toggleSearch(true)}
143120 >
@@ -150,15 +127,18 @@ export default function DocsLayout({ posts, data, content, params }) {
150127
151128export async function getStaticProps({ params }) {
152129 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
130+
153131 const { data, content } = getPage(
154− join(process.cwd(), "content/docs", params.slug.join("/"))
132+ join(process.cwd(), "content/docs", params.slug?.join("/") || "/")
155133 );
134+
156135 return { props: { posts, data, content, params } };
157136}
158137
159138export async function getStaticPaths() {
160139 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
161140 const slugs = [];
141+
162142 const allHrefs = (thisLink, tree) => {
163143 slugs.push(thisLink, ...tree.pages.map((e) => join(thisLink, e.slug)));
164144 allHrefsChildren(thisLink, tree.children);
@@ -169,9 +149,11 @@ export async function getStaticPaths() {
169149 allHrefs(join(thisLink, childSlug), child);
170150 });
171151 };
152+
172153 allHrefs("/docs", posts);
154+
173155 return {
174− paths: slugs.filter((e) => e !== "/docs"),
156+ paths: slugs,
175157 fallback: false,
176158 };
177159}
pages/faq.jsnew file+86
@@ -0,0 +1,86 @@
1+import Head from "next/head";
2+import Link from "next/link";
3+import { useState } from "react";
4+import Container from "../components/Container";
5+import Footer from "../components/Footer";
6+import Header from "../components/Header";
7+import SingleColumn from "../components/SingleColumn";
8+import BackgroundImage from "../components/BackgroundImage";
9+import { getAllPosts, formatDate } from "../lib/lib";
10+
11+export default function Blog({ posts }) {
12+ return (
13+ <Container>
14+ <SingleColumn>
15+ <Header />
16+ <section className="layout-wide">
17+ <div className="measure">
18+ <h2 className="pb-16">Urbit Blog</h2>
19+ <p className="pb-6">
20+ Stories from the broader Urbit community, the Urbit Foundation,
21+ and the many people contributing to Urbit.
22+ </p>
23+ <p>
24+ Subscribe to the Urbit Newsletter for regular updates, including
25+ new blog posts and events.
26+ </p>
27+ </div>
28+ </section>
29+ <section className="layout-wide">
30+ {posts.map((post) => {
31+ return (
32+ <div key={post.slug} className="mb-24 cursor-pointer">
33+ <Link href={`/blog/${post.slug}`}>
34+ <div>
35+ {
36+ // Not all blog posts have images
37+ post.extra.image ? (
38+ <BackgroundImage
39+ src={post.extra.image}
40+ style={{ height: "720px" }}
41+ className="w-full rounded-lg"
42+ />
43+ ) : null
44+ }
45+ <h2 className="mt-4">{post.title}</h2>
46+ <div className="flex items-baseline">
47+ {post.extra.author ? (
48+ <div className="type-ui text-gray mt-4">
49+ {post.extra.author}
50+ </div>
51+ ) : null}
52+ {post.extra.author && post.extra.ship ? (
53+ <div className="mx-1 text-gray">•</div>
54+ ) : null}
55+ {post.extra.ship ? (
56+ <div className="type-ui text-gray font-mono">
57+ {post.extra.ship}
58+ </div>
59+ ) : null}
60+ </div>
61+
62+ <div className="type-ui text-gray mt-2">
63+ {formatDate(new Date(post.date))}
64+ </div>
65+ </div>
66+ </Link>
67+ </div>
68+ );
69+ })}
70+ </section>
71+ </SingleColumn>
72+ <Footer />
73+ </Container>
74+ );
75+}
76+
77+export async function getStaticProps() {
78+ const posts = getPostBySlug(
79+ ["title", "slug", "date", "description", "extra"],
80+ "blog"
81+ );
82+
83+ return {
84+ props: { posts },
85+ };
86+}
pages/grants.js+66−76
@@ -62,21 +62,27 @@ function toggleAll(currentArray, fullSet) {
6262export default function Grants({ posts, categories, types, featuredGrants }) {
6363 const [activeTags, setTags] = useState([]);
6464 const [activeTypes, setTypes] = useState(types);
65+ const [includeCompleted, setIncludeCompleted] = useState(false);
6566 const [tab, setTab] = useState(0);
6667 const [showSearch, toggleSearch] = useState(false);
6768
68− const filteredPosts = posts.reduce((acc, post) => {
69+ const filteredPosts = posts.filter((post) => {
6970 // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
70− const itemDidPass =
71− (post.taxonomies.grant_category.some((r) => activeTags.includes(r)) ||
72− activeTags.length === 0) &&
73− post.taxonomies.grant_type.some((r) => activeTypes.includes(r));
74− // Return the item if the above check has passed
75− if (itemDidPass === true) {
76− return [...acc, post];
77− }
78− return acc;
79− }, []);
71+ const hasCategory = post.taxonomies.grant_category.some((r) =>
72+ activeTags.includes(r)
73+ );
74+ const noTagsSelected = activeTags.length === 0;
75+ const hasType = post.taxonomies.grant_type.some((r) =>
76+ activeTypes.includes(r)
77+ );
78+ const matchesCompletionFilter = includeCompleted
79+ ? true
80+ : post.extra.completed === false;
81+
82+ return (
83+ (hasCategory || noTagsSelected) && hasType && matchesCompletionFilter
84+ );
85+ });
8086
8187 return (
8288 <Container toggleSearch={() => toggleSearch((state) => !state)}>
@@ -128,12 +134,12 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
128134 <div className="flex flex-col md:flex-row md:space-x-4 md:space-y-0 space-y-4 space-x-0">
129135 {featuredGrants.map((grant) => {
130136 return (
131− <Link href={grant.slug}>
137+ <Link href={"/grants/" + grant.slug}>
132138 <div className="p-8 bg-washedGreen rounded-lg w-full md:w-1/3 cursor-pointer">
133139 <h4 className="pb-4">{grant.title}</h4>
134140 <p className="text-green pb-4">
135141 {grant.extra.reward} star
136− {grant.extra.reward > 1 ? "s" : ""}{" "}
142+ {grant.extra.reward === 1 ? "" : "s"}{" "}
137143 {grant.extra.completed ? "awarded" : "pending"}
138144 </p>
139145 <p>{grant.extra.description}</p>
@@ -143,70 +149,53 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
143149 })}
144150 </div>
145151 </section>
152+
153+ {
154+ // Gift Grants
155+ }
156+ <section className="layout-wide">
157+ <div className="measure pb-16">
158+ <h2 className="pb-16">Gifts</h2>
159+ <p className="mb-8">
160+ Gifts are given post facto for exceptional contributions.
161+ </p>
162+ </div>
163+ </section>
146164 {
147165 // Find a Grant
148166 }
149167 <section className="layout-wide">
150168 <h2 className="pb-8">Find A Grant</h2>
151− <div className="flex items-center measure pb-8">
169+ <div className="flex items-center measure pb-2">
152170 <button
153171 onClick={() => {
154172 setTab(0);
155173 setTypes(types);
156174 }}
157− className={`tab-button-sm mr-4 ${
158− tab === 0 ? "text-black" : "text-gray"
175+ className={`badge-lg mr-2 ${
176+ tab === 0 ? "text-white bg-black" : "text-gray bg-wall"
159177 }`}
160178 >
161179 All
162180 </button>
163− {
164− // NB Gavin: These should be auto-generated from the markdown metadata
165− }
166− <button
167− onClick={() => {
168− setTab(1);
169− setTypes(["Proposal"]);
170− }}
171− className={`tab-button-sm mr-4 ${
172− tab === 1 ? "text-black" : "text-gray"
173− }`}
174− >
175− Proposals
176− </button>
177− <button
178− onClick={() => {
179− setTab(2);
180− setTypes(["Bounty"]);
181− }}
182− className={`tab-button-sm mr-4 ${
183− tab === 2 ? "text-black" : "text-gray"
184− }`}
185− >
186− Bounties
187− </button>
188− <button
189− onClick={() => {
190− setTab(3);
191− setTypes(["Apprenticeship"]);
192− }}
193− className={`tab-button-sm mr-4 ${
194− tab === 3 ? "text-black" : "text-gray"
195− }`}
196− >
197− Apprenticeships
198− </button>
199− <button
200− onClick={() => {
201− setTab(4);
202− setTypes(["Gift"]);
203− }}
204− className={`tab-button-sm ${
205− tab === 4 ? "text-black" : "text-gray"
206− }`}
207− >
208− Gifts
209− </button>
181+ {types.map((type, index) => {
182+ return (
183+ <button
184+ onClick={() => {
185+ // + 1 is added here because the 'all' button precedes this sequence
186+ setTab(index + 1);
187+ setTypes([type]);
188+ }}
189+ className={`badge-lg mr-2 ${
190+ tab === index + 1
191+ ? "text-white bg-black"
192+ : "text-gray bg-wall"
193+ }`}
194+ >
195+ {type}
196+ </button>
197+ );
198+ })}
210199 </div>
211200
212201 <div className="flex flex-wrap mb-12">
@@ -227,17 +216,18 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
227216 })}
228217 </div>
229218
230− {
231− // <div className="pb-8">
232− // <TabCarousel index={tab}>
233− // <p>All {filteredPosts.length} Urbit grants</p>
234− // <p>{filteredPosts.length} proposals from community members to work on their own projects</p>
235− // <p>{filteredPosts.length} bounties for specific work needed by the Urbit Foundation</p>
236− // <p>{filteredPosts.length} apprenticeships for projects undertaken with a mentor</p>
237− // <p>{filteredPosts.length} gifts given post facto for exceptional contributions</p>
238− // </TabCarousel>
239− // </div>
240− }
219+ <div className="pb-8 flex">
220+ <h4>
221+ Showing {filteredPosts.length} grant
222+ {filteredPosts.length === 1 ? "" : "s"}
223+ </h4>
224+ <button
225+ className="ml-4"
226+ onClick={() => setIncludeCompleted(!includeCompleted)}
227+ >
228+ {includeCompleted ? "Exclude Completed" : "Include Completed"}
229+ </button>
230+ </div>
241231
242232 {filteredPosts.map((post) => {
243233 return (
@@ -253,9 +243,9 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
253243 <p className="text-gray">
254244 <strong>Reward: </strong>
255245 {post.extra.reward} star
256− {post.extra.reward > 1 ? "s" : ""}
246+ {post.extra.reward === 1 ? "" : "s"}
257247 </p>
258− <div className="flex">
248+ <div className="flex items-center">
259249 {post.taxonomies.grant_type.map((category) => (
260250 <div className="bg-black text-wall badge-sm m-1">
261251 {category}
pages/grants/[slug].jsnew file+146
@@ -0,0 +1,146 @@
1+import { useRouter } from "next/router";
2+import {
3+ getPostBySlug,
4+ getAllPosts,
5+ getNextPost,
6+ getPreviousPost,
7+ formatDate,
8+} from "../../lib/lib";
9+import Head from "next/head";
10+import Link from "next/link";
11+import ErrorPage from "../404";
12+import Container from "../../components/Container";
13+import Markdown from "../../components/Markdown";
14+import Header from "../../components/Header";
15+import Footer from "../../components/Footer";
16+import BackgroundImage from "../../components/BackgroundImage";
17+import SingleColumn from "../../components/SingleColumn";
18+import NewsletterSignup from "../../components/NewletterSignup";
19+import PostPreview from "../../components/PostPreview";
20+import { name, contact } from "../../lib/constants";
21+
22+export default function Grant({ post, nextPost, previousPost }) {
23+ const router = useRouter();
24+ if (!router.isFallback && !post?.slug) {
25+ return <ErrorPage />;
26+ }
27+
28+ return (
29+ <Container>
30+ <SingleColumn>
31+ <Header />
32+ <section className="flex flex-col layout-wide">
33+ <h1>{post.title}</h1>
34+ {post.extra.author ? (
35+ <div className="type-ui text-gray mt-4">{post.extra.author}</div>
36+ ) : null}
37+ {post.extra.ship ? (
38+ <div className="type-ui text-gray font-mono">{post.extra.ship}</div>
39+ ) : null}
40+ <div className="type-ui text-gray mt-8 md:mt-12 lg:mt-16">
41+ {formatDate(new Date(post.date))}
42+ </div>
43+ </section>
44+ <article className="layout-wide">
45+ <Markdown post={post} />
46+ </article>
47+ <section className="layout-narrow">
48+ <div className="measure">
49+ <h4 className="pb-6">
50+ If you’d like to follow our progress, we send monthly updates via
51+ email:
52+ </h4>
53+ </div>
54+ <NewsletterSignup color="black" />
55+ <h4 className="mt-12 text-gray">
56+ Follow us on{" "}
57+ <a className="text-black" href={contact.twitter}>
58+ Twitter
59+ </a>
60+ </h4>
61+ <h4 className="mt-6 text-gray">
62+ Explore code on{" "}
63+ <a className="text-black" href={contact.github}>
64+ Github
65+ </a>
66+ </h4>
67+ <h4 className="mt-6 text-gray">
68+ Ask questions in our{" "}
69+ <a className="text-black" href={contact.discord}>
70+ Discord
71+ </a>
72+ </h4>
73+ <h4 className="mt-6 text-gray">
74+ Boot Urbit and join{" "}
75+ <code className="bg-wall p-2 rounded-lg">
76+ {contact.urbitCommunity}
77+ </code>
78+ </h4>
79+ </section>
80+ <section className="layout-wide flex">
81+ {previousPost === null ? (
82+ <div className={"w-1/2 mr-4"} />
83+ ) : (
84+ <PostPreview
85+ title="Previous Post"
86+ post={previousPost}
87+ className="mr-4 w-1/2"
88+ />
89+ )}
90+ {nextPost === null ? (
91+ <div className={"w-1/2 ml-4"} />
92+ ) : (
93+ <PostPreview
94+ title="Next Post"
95+ post={nextPost}
96+ className="ml-4 w-1/2"
97+ />
98+ )}
99+ </section>
100+ </SingleColumn>
101+ <Footer />
102+ </Container>
103+ );
104+}
105+
106+//
107+export async function getStaticProps({ params }) {
108+ const nextPost =
109+ getNextPost(
110+ params.slug,
111+ ["title", "slug", "date", "description", "extra"],
112+ "grants"
113+ ) || null;
114+
115+ const previousPost =
116+ getPreviousPost(
117+ params.slug,
118+ ["title", "slug", "date", "description", "extra"],
119+ "grants"
120+ ) || null;
121+
122+ const post = getPostBySlug(
123+ params.slug,
124+ ["title", "slug", "date", "description", "content", "extra"],
125+ "grants"
126+ );
127+
128+ return {
129+ props: { post, nextPost, previousPost },
130+ };
131+}
132+
133+export async function getStaticPaths() {
134+ const posts = getAllPosts(["slug", "date"], "grants");
135+
136+ return {
137+ paths: posts.map((post) => {
138+ return {
139+ params: {
140+ slug: post.slug,
141+ },
142+ };
143+ }),
144+ fallback: false,
145+ };
146+}
pages/index.js+6−8
@@ -59,15 +59,13 @@ export default function Home({ posts, events, openGrantsCount }) {
5959 // Grants
6060 }
6161 <section className="layout-narrow">
62− <div className="flex flex-col md:flex-row md:items-center measure pb-12">
62+ <div className="flex flex-col md:flex-row md:items-center items-start measure pb-12">
6363 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
64− <div>
65− <Link href="/grants">
66− <a className="bg-green text-white badge-lg">
67− {openGrantsCount} Open
68− </a>
69− </Link>
70− </div>
64+ <Link href="/grants">
65+ <a className="bg-green text-white badge-lg">
66+ {openGrantsCount} Open
67+ </a>
68+ </Link>
7169 </div>
7270 <div className="flex flex-col md:flex-row w-full items-center bg-washedGreen px-8 py-8 rounded-xl">
7371 <div className="flex items-center flex-col p-4 w-full">
styles/globals.css+10−7
@@ -31,12 +31,12 @@ p a {
3131
3232h1,
3333.type-h1 {
34− @apply text-4xl md:text-5xl lg:text-6xl font-bold leading-relaxed;
34+ @apply text-4xl md:text-5xl lg:text-6xl font-bold leading-tight;
3535}
3636
3737h2,
3838.type-h2 {
39− @apply text-3xl md:text-4xl lg:text-5xl font-bold;
39+ @apply text-3xl md:text-4xl lg:text-5xl font-bold leading-tight;
4040}
4141
4242h3,
@@ -88,7 +88,7 @@ button {
8888}
8989
9090.badge-lg {
91− @apply flex type-ui px-4 lg:px-6 items-center justify-center h-12 md:h-16 rounded-full;
91+ @apply flex type-ui px-4 lg:px-6 items-center justify-center h-10 md:h-14 rounded-full;
9292}
9393
9494.tab-button-lg {
@@ -152,9 +152,12 @@ a:hover {
152152}
153153
154154.dot::before {
155− content: "·";
155+ width: 0.5rem;
156+ height: 0.5rem;
157+ border-radius: 99px;
158+ content: "";
156159 position: absolute;
157− left: -16px;
158− margin-top: -2px;
159− font-size: 3rem;
160+ left: -1.25rem;
161+ top: 0.4rem;
162+ @apply bg-green;
160163}
tailwind.config.js+14
@@ -22,6 +22,20 @@ module.exports = {
2222 sans: ["Inter", "-apple-system", "sans-serif"],
2323 mono: ["Source Code Pro", "monospace"],
2424 },
25+ extend: {
26+ fontSize: {
27+ xs: "0.75rem",
28+ sm: "0.875rem",
29+ base: "1rem",
30+ lg: "1.125rem",
31+ xl: "1.25rem",
32+ "2xl": "1.5rem",
33+ "3xl": "1.875rem",
34+ "4xl": "2.25rem",
35+ "5xl": "3rem",
36+ "6xl": "4rem",
37+ },
38+ },
2539 },
2640 variants: {
2741 extend: {},