on master help sign in
reorganize docs routing and sidebar
commit4d9795016b656b04ed197399411c8315596f6279 authorGavin Atkinson <gvnatknsn@protonmail.com> dateFri 25 Jun 2021, 14:58 (5 years ago) parentb9b839d
17 files changed+497−294
components/ContactSection.js+38 components/ContentArea.js+18 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+2−2 pages/docs.js−120 pages/docs/[...slug].js → pages/docs/[[...slug]].js+43−55 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+18
@@ -0,0 +1,18 @@
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 className="button-sm bg-wall text-gray">
7+ Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
8+ </button>
9+ </header>
10+ <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
11+ <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
12+ <h2 className="mb-16 mt-4">{props.title}</h2>
13+ {props.children}
14+ <div className="pb-32" />
15+ </div>
16+ </div>
17+ );
18+}
components/Header.js+1−1
@@ -50,7 +50,7 @@ function MenuTray(props) {
5050export default function Header() {
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+2−2
@@ -8,14 +8,14 @@ import SingleColumn from "../components/SingleColumn";
88import BackgroundImage from "../components/BackgroundImage";
99import { getAllPosts, formatDate } from "../lib/lib";
1010
11−export default function Blog({ posts }) {
11+export default function Faq({ posts }) {
1212 return (
1313 <Container>
1414 <SingleColumn>
1515 <Header />
1616 <section className="layout-wide">
1717 <div className="measure">
18− <h2 className="pb-16">Urbit Blog</h2>
18+ <h2 className="pb-16">FAQ</h2>
1919 <p className="pb-6">
2020 Stories from the broader Urbit community, the Urbit Foundation,
2121 and the many people contributing to Urbit.
pages/docs.jsdeleted−120
@@ -1,120 +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−
9−const breadcrumbs = (posts, paths) => {
10− const results = [
11− <Link href="/">Urbit.org</Link>,
12− <span className="px-1">/</span>,
13− <Link href="/docs">Documentation</Link>,
14− ];
15− let thisLink = "/docs";
16− for (const path of paths) {
17− posts = posts.children[path];
18− thisLink = join(thisLink, path);
19− results.push(
20− <span className="px-1">/</span>,
21− <Link href={thisLink}>{posts.title}</Link>
22− );
23− }
24− return results;
25−};
26−
27−const childPages = (thisLink, children, level = 0) => (
28− <ul className="pl-1">
29− {Object.entries(children).map(([childSlug, child]) => (
30− <li>{pageTree(join(thisLink, childSlug), child, level)}</li>
31− ))}
32− </ul>
33−);
34−
35−const pageTree = (thisLink, tree, level = 0) => {
36− const [isOpen, toggleTree] = useState(false);
37−
38− const activeClasses = classnames({
39− hidden: !isOpen,
40− });
41− return (
42− <>
43− <span
44− className={
45− "font-medium text-gray text-lg cursor-pointer hover:text-black"
46− }
47− onClick={() => toggleTree(!isOpen)}
48− >
49− {tree.title}
50− </span>
51− <div className={activeClasses}>
52− <ul className={"pl-1"}>
53− {tree.pages
54− .sort((a, b) => a.weight - b.weight)
55− .map(({ title, slug }) => (
56− <li className="font-thin py-1">
57− <Link href={join(thisLink, slug)}>{title}</Link>
58− </li>
59− ))}
60− </ul>
61− {childPages(thisLink, tree.children, level + 1)}
62− </div>
63− </>
64− );
65−};
66−
67−function Sidebar(props) {
68− return (
69− <div className="flex flex-col w-72 bg-wall max-h-screen h-screen">
70− <header className="flex flex-shrink-0 justify-between items-center pl-12 pt-12 pb-8">
71− <Link href="/">
72− <a className="type-ui text-gray">Urbit.org</a>
73− </Link>
74− </header>
75− <div className="overflow-y-scroll p-12 pt-16">
76− {props.children}
77− <div className="pb-32" />
78− </div>
79− </div>
80− );
81−}
82−
83−function ContentArea(props) {
84− return (
85− <div className="w-full">
86− <header className="flex justify-between items-center px-24 pt-12 pb-8">
87− <div className="type-ui">Urbit Documentation</div>
88− <button className="button-sm bg-wall text-gray">
89− Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
90− </button>
91− </header>
92− <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
93− <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
94− <h2 className="mb-16 mt-4">{props.title}</h2>
95− {props.children}
96− <div className="pb-32" />
97− </div>
98− </div>
99− );
100−}
101−
102−export default function DocsLayout({ posts, data, content }) {
103− return (
104− <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
105− <Head>
106− <title>Documentation / Urbit.org</title>
107− </Head>
108− <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
109− <ContentArea breadcrumbs={breadcrumbs(posts, [])} title={data.title}>
110− <Markdown post={{ content: content }} />
111− </ContentArea>
112− </div>
113− );
114−}
115−
116−export async function getStaticProps() {
117− const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
118− const { data, content } = getPage(join(process.cwd(), "content/docs"));
119− return { props: { posts, data, content } };
120−}
pages/docs/[...slug].js → pages/docs/[[...slug]].jsrenamed+43−55
@@ -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";
911
1012const breadcrumbs = (posts, paths) => {
1113 const results = [
@@ -38,37 +40,53 @@ const pageTree = (thisLink, tree, level = 0) => {
3840 const firstCrumb = "/" + router.asPath.split("/").slice(1, -1).join("/");
3941
4042 const includesThisPage = firstCrumb.includes(thisLink);
41− const isThisPage = firstCrumb === thisLink;
43+ const isThisPage = router.asPath === thisLink;
4244 const [isOpen, toggleTree] = useState(includesThisPage);
4345
4446 const activeClasses = classnames({
4547 hidden: !isOpen,
4648 });
4749
48− const headerClass = classnames({
49− "text-gray": !isThisPage,
50+ const headingItemClasses = classnames({
51+ "pl-0 text-black text-base font-semibold hover:text-green leading-relaxed":
52+ level === 0,
53+ "pl-4 text-black text-base font-semibold hover:text-green": level === 1,
54+ "pl-8 text-black text-base hover:text-green": level === 2,
55+ });
56+
57+ const pageItemClasses = classnames({
58+ "pl-4 text-black text-base hover:text-green": level === 0,
59+ "pl-8 text-black text-base hover:text-green": level === 1,
60+ "pl-12 text-black text-base hover:text-green": level === 2,
5061 dot: isThisPage,
5162 });
63+
5264 return (
5365 <>
54− <span
55− className={
56− "font-medium text-lg cursor-pointer hover:text-black relative " +
57− headerClass
58− }
59− onClick={() => toggleTree(!isOpen)}
60− >
61− {tree.title}
66+ <span onClick={() => toggleTree(!isOpen)}>
67+ <p className={`${headingItemClasses} cursor-pointer`}>{tree.title}</p>
6268 </span>
6369 <div className={activeClasses}>
6470 <ul className={"pl-1"}>
65− {tree.pages
66− .sort((a, b) => a.weight - b.weight)
67− .map(({ title, slug }) => (
68− <li className="font-thin py-1">
69− <Link href={join(thisLink, slug)}>{title}</Link>
71+ {tree.pages.map(({ title, slug }) => {
72+ const href = join(thisLink, slug);
73+ const isSelected = router.asPath === href;
74+ const selectedClasses = classnames({
75+ dot: isSelected,
76+ "text-green": isSelected,
77+ });
78+ return (
79+ <li>
80+ <Link href={href}>
81+ <a
82+ className={`relative ${pageItemClasses} ${selectedClasses}`}
83+ >
84+ {title}
85+ </a>
86+ </Link>
7087 </li>
71− ))}
88+ );
89+ })}
7290 </ul>
7391 {childPages(thisLink, tree.children, level + 1)}
7492 </div>
@@ -76,41 +94,6 @@ const pageTree = (thisLink, tree, level = 0) => {
7694 );
7795};
7896
79−function Sidebar(props) {
80− return (
81− <div className="flex flex-col w-72 bg-wall max-h-screen h-screen">
82− <header className="flex flex-shrink-0 justify-between items-center pl-12 pt-12 pb-8">
83− <Link href="/">
84− <a className="type-ui text-gray">Urbit.org</a>
85− </Link>
86− </header>
87− <div className="overflow-y-scroll p-12 pt-16">
88− {props.children}
89− <div className="pb-32" />
90− </div>
91− </div>
92− );
93−}
94−
95−function ContentArea(props) {
96− return (
97− <div className="w-full">
98− <header className="flex justify-between items-center px-24 pt-12 pb-8">
99− <div className="type-ui">Urbit Documentation</div>
100− <button className="button-sm bg-wall text-gray">
101− Search Urbit.org<div className="ml-4 text-lightGray">⌘K</div>
102− </button>
103− </header>
104− <div className="px-24 pb-24 pt-16 flex flex-col w-full max-h-screen h-screen overflow-y-scroll">
105− <div className="type-ui text-lightGray">{props.breadcrumbs}</div>
106− <h2 className="mb-16 mt-4">{props.title}</h2>
107− {props.children}
108− <div className="pb-32" />
109− </div>
110− </div>
111− );
112−}
113−
11497export default function DocsLayout({ posts, data, content, params }) {
11598 return (
11699 <>
@@ -120,7 +103,7 @@ export default function DocsLayout({ posts, data, content, params }) {
120103 <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
121104 <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
122105 <ContentArea
123− breadcrumbs={breadcrumbs(posts, params.slug.slice(0, -1))}
106+ breadcrumbs={breadcrumbs(posts, params.slug?.slice(0, -1) || "")}
124107 title={data.title}
125108 >
126109 <Markdown post={{ content: content }} />
@@ -132,15 +115,18 @@ export default function DocsLayout({ posts, data, content, params }) {
132115
133116export async function getStaticProps({ params }) {
134117 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
118+
135119 const { data, content } = getPage(
136− join(process.cwd(), "content/docs", params.slug.join("/"))
120+ join(process.cwd(), "content/docs", params.slug?.join("/") || "/")
137121 );
122+
138123 return { props: { posts, data, content, params } };
139124}
140125
141126export async function getStaticPaths() {
142127 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
143128 const slugs = [];
129+
144130 const allHrefs = (thisLink, tree) => {
145131 slugs.push(thisLink, ...tree.pages.map((e) => join(thisLink, e.slug)));
146132 allHrefsChildren(thisLink, tree.children);
@@ -151,9 +137,11 @@ export async function getStaticPaths() {
151137 allHrefs(join(thisLink, childSlug), child);
152138 });
153139 };
140+
154141 allHrefs("/docs", posts);
142+
155143 return {
156− paths: slugs.filter((e) => e !== "/docs"),
144+ paths: slugs,
157145 fallback: false,
158146 };
159147}
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
@@ -61,20 +61,26 @@ function toggleAll(currentArray, fullSet) {
6161export default function Grants({ posts, categories, types, featuredGrants }) {
6262 const [activeTags, setTags] = useState([]);
6363 const [activeTypes, setTypes] = useState(types);
64+ const [includeCompleted, setIncludeCompleted] = useState(false);
6465 const [tab, setTab] = useState(0);
6566
66− const filteredPosts = posts.reduce((acc, post) => {
67+ const filteredPosts = posts.filter((post) => {
6768 // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
68− const itemDidPass =
69− (post.taxonomies.grant_category.some((r) => activeTags.includes(r)) ||
70− activeTags.length === 0) &&
71− post.taxonomies.grant_type.some((r) => activeTypes.includes(r));
72− // Return the item if the above check has passed
73− if (itemDidPass === true) {
74− return [...acc, post];
75− }
76− return acc;
77− }, []);
69+ const hasCategory = post.taxonomies.grant_category.some((r) =>
70+ activeTags.includes(r)
71+ );
72+ const noTagsSelected = activeTags.length === 0;
73+ const hasType = post.taxonomies.grant_type.some((r) =>
74+ activeTypes.includes(r)
75+ );
76+ const matchesCompletionFilter = includeCompleted
77+ ? true
78+ : post.extra.completed === false;
79+
80+ return (
81+ (hasCategory || noTagsSelected) && hasType && matchesCompletionFilter
82+ );
83+ });
7884
7985 return (
8086 <Container>
@@ -123,12 +129,12 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
123129 <div className="flex flex-col md:flex-row md:space-x-4 md:space-y-0 space-y-4 space-x-0">
124130 {featuredGrants.map((grant) => {
125131 return (
126− <Link href={grant.slug}>
132+ <Link href={"/grants/" + grant.slug}>
127133 <div className="p-8 bg-washedGreen rounded-lg w-full md:w-1/3 cursor-pointer">
128134 <h4 className="pb-4">{grant.title}</h4>
129135 <p className="text-green pb-4">
130136 {grant.extra.reward} star
131− {grant.extra.reward > 1 ? "s" : ""}{" "}
137+ {grant.extra.reward === 1 ? "" : "s"}{" "}
132138 {grant.extra.completed ? "awarded" : "pending"}
133139 </p>
134140 <p>{grant.extra.description}</p>
@@ -138,70 +144,53 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
138144 })}
139145 </div>
140146 </section>
147+
148+ {
149+ // Gift Grants
150+ }
151+ <section className="layout-wide">
152+ <div className="measure pb-16">
153+ <h2 className="pb-16">Gifts</h2>
154+ <p className="mb-8">
155+ Gifts are given post facto for exceptional contributions.
156+ </p>
157+ </div>
158+ </section>
141159 {
142160 // Find a Grant
143161 }
144162 <section className="layout-wide">
145163 <h2 className="pb-8">Find A Grant</h2>
146− <div className="flex items-center measure pb-8">
164+ <div className="flex items-center measure pb-2">
147165 <button
148166 onClick={() => {
149167 setTab(0);
150168 setTypes(types);
151169 }}
152− className={`tab-button-sm mr-4 ${
153− tab === 0 ? "text-black" : "text-gray"
170+ className={`badge-lg mr-2 ${
171+ tab === 0 ? "text-white bg-black" : "text-gray bg-wall"
154172 }`}
155173 >
156174 All
157175 </button>
158− {
159− // NB Gavin: These should be auto-generated from the markdown metadata
160− }
161− <button
162− onClick={() => {
163− setTab(1);
164− setTypes(["Proposal"]);
165− }}
166− className={`tab-button-sm mr-4 ${
167− tab === 1 ? "text-black" : "text-gray"
168− }`}
169− >
170− Proposals
171− </button>
172− <button
173− onClick={() => {
174− setTab(2);
175− setTypes(["Bounty"]);
176− }}
177− className={`tab-button-sm mr-4 ${
178− tab === 2 ? "text-black" : "text-gray"
179− }`}
180− >
181− Bounties
182− </button>
183− <button
184− onClick={() => {
185− setTab(3);
186− setTypes(["Apprenticeship"]);
187− }}
188− className={`tab-button-sm mr-4 ${
189− tab === 3 ? "text-black" : "text-gray"
190− }`}
191− >
192− Apprenticeships
193− </button>
194− <button
195− onClick={() => {
196− setTab(4);
197− setTypes(["Gift"]);
198− }}
199− className={`tab-button-sm ${
200− tab === 4 ? "text-black" : "text-gray"
201− }`}
202− >
203− Gifts
204− </button>
176+ {types.map((type, index) => {
177+ return (
178+ <button
179+ onClick={() => {
180+ // + 1 is added here because the 'all' button precedes this sequence
181+ setTab(index + 1);
182+ setTypes([type]);
183+ }}
184+ className={`badge-lg mr-2 ${
185+ tab === index + 1
186+ ? "text-white bg-black"
187+ : "text-gray bg-wall"
188+ }`}
189+ >
190+ {type}
191+ </button>
192+ );
193+ })}
205194 </div>
206195
207196 <div className="flex flex-wrap mb-12">
@@ -222,17 +211,18 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
222211 })}
223212 </div>
224213
225− {
226− // <div className="pb-8">
227− // <TabCarousel index={tab}>
228− // <p>All {filteredPosts.length} Urbit grants</p>
229− // <p>{filteredPosts.length} proposals from community members to work on their own projects</p>
230− // <p>{filteredPosts.length} bounties for specific work needed by the Urbit Foundation</p>
231− // <p>{filteredPosts.length} apprenticeships for projects undertaken with a mentor</p>
232− // <p>{filteredPosts.length} gifts given post facto for exceptional contributions</p>
233− // </TabCarousel>
234− // </div>
235− }
214+ <div className="pb-8 flex">
215+ <h4>
216+ Showing {filteredPosts.length} grant
217+ {filteredPosts.length === 1 ? "" : "s"}
218+ </h4>
219+ <button
220+ className="ml-4"
221+ onClick={() => setIncludeCompleted(!includeCompleted)}
222+ >
223+ {includeCompleted ? "Exclude Completed" : "Include Completed"}
224+ </button>
225+ </div>
236226
237227 {filteredPosts.map((post) => {
238228 return (
@@ -248,9 +238,9 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
248238 <p className="text-gray">
249239 <strong>Reward: </strong>
250240 {post.extra.reward} star
251− {post.extra.reward > 1 ? "s" : ""}
241+ {post.extra.reward === 1 ? "" : "s"}
252242 </p>
253− <div className="flex">
243+ <div className="flex items-center">
254244 {post.taxonomies.grant_type.map((category) => (
255245 <div className="bg-black text-wall badge-sm m-1">
256246 {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
@@ -57,15 +57,13 @@ export default function Home({ posts, events, openGrantsCount }) {
5757 // Grants
5858 }
5959 <section className="layout-narrow">
60− <div className="flex flex-col md:flex-row md:items-center measure pb-12">
60+ <div className="flex flex-col md:flex-row md:items-center items-start measure pb-12">
6161 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
62− <div>
63− <Link href="/grants">
64− <a className="bg-green text-white badge-lg">
65− {openGrantsCount} Open
66− </a>
67− </Link>
68− </div>
62+ <Link href="/grants">
63+ <a className="bg-green text-white badge-lg">
64+ {openGrantsCount} Open
65+ </a>
66+ </Link>
6967 </div>
7068 <div className="flex flex-col md:flex-row w-full items-center bg-washedGreen px-8 py-8 rounded-xl">
7169 <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: {},