on master help sign in
Merge pull request #668 from urbit/urbit.org2-docs-ga
aa9f8abGavin
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) {
50export default function Header(props) {
50export default function Header(props) {
51 const [isOpen, toggleTray] = useState(false);
51 const [isOpen, toggleTray] = useState(false);
52 return (
52 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">
54 <div className="absolute z-10">
54 <div className="absolute z-10">
55 <Link href="/">
55 <Link href="/">
56 <a className="type-ui">Urbit</a>
56 <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
4description = "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."
4description = "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."
5[taxonomies]
5[taxonomies]
6grant_type = ["Gift"]
6grant_type = ["Gift"]
7+grant_category = []
7[extra]
8[extra]
8author = "Josh Lehman"
9author = "Josh Lehman"
9ship = "~wolref-podlex"
10ship = "~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
5aliases = ["/posts/essays/urbit-grants-and-mid-2019-gifts", "/posts/urbit-grants-and-mid-2019-gifts"]
5aliases = ["/posts/essays/urbit-grants-and-mid-2019-gifts", "/posts/urbit-grants-and-mid-2019-gifts"]
6[taxonomies]
6[taxonomies]
7grant_type = ["Gift"]
7grant_type = ["Gift"]
8+grant_category = []
8[extra]
9[extra]
9author = "Galen Wolfe-Pauly + Alex Matzner"
10author = "Galen Wolfe-Pauly + Alex Matzner"
10ship = "~ravmel-ropdyl + ~mignyt-mogseb"
11ship = "~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."
5aliases = ["/posts/2018-2-17-update/"]
5aliases = ["/posts/2018-2-17-update/"]
6[taxonomies]
6[taxonomies]
7grant_type = ["Gift"]
7grant_type = ["Gift"]
8+grant_category = []
8[extra]
9[extra]
9ship = "~tonlur-sarret"
10ship = "~tonlur-sarret"
10author = ""
11author = ""
lib/lib.js+47−25
@@ -5,11 +5,6 @@ import { join, parse } from "path";
5import matter from "gray-matter";
5import matter from "gray-matter";
6import toml from "@iarna/toml";
6import toml from "@iarna/toml";
7
7
8−// export async function markdownToHtml(markdown) {
9−// const result = await remark().use(html).process(markdown)
10−// return result.toString()
11−// }
12−
13const directories = {
8const directories = {
14 blog: join(process.cwd(), "content/blog"),
9 blog: join(process.cwd(), "content/blog"),
15 events: join(process.cwd(), "content/events"),
10 events: join(process.cwd(), "content/events"),
@@ -69,6 +64,26 @@ export function getAllPosts(fields = [], key) {
69 return posts;
64 return posts;
70}
65}
71
66
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+
72export function getNextPost(slug, fields = [], key) {
87export function getNextPost(slug, fields = [], key) {
73 let resultPost = null;
88 let resultPost = null;
74
89
@@ -144,35 +159,42 @@ export function formatDate(d) {
144// children: { [string]: PageTree },
159// children: { [string]: PageTree },
145// }
160// }
146
161
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.
148 const metadata = matter(fs.readFileSync(join(path, "_index.md")), options);
164 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
150 const children = fs.readdirSync(path, { withFileTypes: true });
166 const children = fs.readdirSync(path, { withFileTypes: true });
167+ // get a list of files
151 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
168 const pages = children.filter((f) => f.isFile() && f.name !== "_index.md");
169+ // get a list of folders
152 const subdirs = children.filter((f) => f.isDirectory());
170 const subdirs = children.filter((f) => f.isDirectory());
153−
171+ // return a pagetree datastructure
154 return {
172 return {
155 title: metadata.data.title,
173 title: metadata.data.title,
156− pages: pages.map((page) => {
174+ pages: pages
157− const { data: pageData, content: pageContent } = matter(
175+ .map((page) => {
176+ // retrieve topmatter as JSON
177+ const { content, data } = matter(
158− fs.readFileSync(join(path, page.name)),
178+ fs.readFileSync(join(path, page.name)),
159− options
179+ options
160− );
180+ );
181+
161− return {
182+ return {
162− title: pageData.title,
183+ title: data.title,
163− base: page.name,
184+ base: page.name,
164− slug: page.name.replace(/.md$/, ""),
185+ slug: page.name.replace(/.md$/, ""),
165− ...(type === "weight" && { weight: pageData?.weight ?? 0 }),
186+ ...(ordering === "weight" && { weight: data?.weight ?? 0 }),
166− ...(type === "date" && {
187+ ...(ordering === "date" && {
167− date: pageData?.date ?? "2000-01-01T00:00:00.000Z",
188+ date: data?.date ?? "2000-01-01T00:00:00.000Z",
168− }),
189+ }),
169− ...(content && { content: pageContent }),
190+ };
170− };
191+ })
171− }),
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),
172 children: Object.fromEntries(
194 children: Object.fromEntries(
173 subdirs.map((subdir) => [
195 subdirs.map((subdir) => [
174 subdir.name,
196 subdir.name,
175− buildPageTree(join(path, subdir.name), type, content),
197+ buildPageTree(join(path, subdir.name), ordering, content),
176 ])
198 ])
177 ),
199 ),
178 };
200 };
pages/blog.js+1−1
@@ -18,7 +18,7 @@ export default function Blog({ posts }) {
18 <Header toggleSearch={() => toggleSearch(true)} />
18 <Header toggleSearch={() => toggleSearch(true)} />
19 <section className="layout-wide">
19 <section className="layout-wide">
20 <div className="measure">
20 <div className="measure">
21− <h2 className="pb-16">Urbit Blog</h2>
21+ <h2 className="pb-16">FAQ</h2>
22 <p className="pb-6">
22 <p className="pb-6">
23 Stories from the broader Urbit community, the Urbit Foundation,
23 Stories from the broader Urbit community, the Urbit Foundation,
24 and the many people contributing to Urbit.
24 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";
6import { join } from "path";
6import { join } from "path";
7import { getDocs, formatDate, buildPageTree, getPage } from "../../lib/lib";
7import { getDocs, formatDate, buildPageTree, getPage } from "../../lib/lib";
8import Markdown from "../../components/Markdown";
8import Markdown from "../../components/Markdown";
9+import ContentArea from "../../components/ContentArea";
10+import Sidebar from "../../components/Sidebar";
9import Search from "../../components/Search";
11import Search from "../../components/Search";
10import { configure, GlobalHotKeys } from "react-hotkeys";
12import { configure, GlobalHotKeys } from "react-hotkeys";
11
13
@@ -40,37 +42,53 @@ const pageTree = (thisLink, tree, level = 0) => {
40 const firstCrumb = "/" + router.asPath.split("/").slice(1, -1).join("/");
42 const firstCrumb = "/" + router.asPath.split("/").slice(1, -1).join("/");
41
43
42 const includesThisPage = firstCrumb.includes(thisLink);
44 const includesThisPage = firstCrumb.includes(thisLink);
43− const isThisPage = firstCrumb === thisLink;
45+ const isThisPage = router.asPath === thisLink;
44 const [isOpen, toggleTree] = useState(includesThisPage);
46 const [isOpen, toggleTree] = useState(includesThisPage);
45
47
46 const activeClasses = classnames({
48 const activeClasses = classnames({
47 hidden: !isOpen,
49 hidden: !isOpen,
48 });
50 });
49
51
50− const headerClass = classnames({
52+ const headingItemClasses = classnames({
51− "text-gray": !isThisPage,
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,
52 dot: isThisPage,
63 dot: isThisPage,
53 });
64 });
65+
54 return (
66 return (
55 <>
67 <>
56− <span
57− className={
58− "font-medium text-lg cursor-pointer hover:text-black relative " +
59− headerClass
60− }
61− onClick={() => toggleTree(!isOpen)}
68+ <span onClick={() => toggleTree(!isOpen)}>
62− >
69+ <p className={`${headingItemClasses} cursor-pointer`}>{tree.title}</p>
63− {tree.title}
64 </span>
70 </span>
65 <div className={activeClasses}>
71 <div className={activeClasses}>
66 <ul className={"pl-1"}>
72 <ul className={"pl-1"}>
67− {tree.pages
68− .sort((a, b) => a.weight - b.weight)
69− .map(({ title, slug }) => (
73+ {tree.pages.map(({ title, slug }) => {
70− <li className="font-thin py-1">
71− <Link href={join(thisLink, slug)}>{title}</Link>
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>
72 </li>
89 </li>
90+ );
73− ))}
91+ })}
74 </ul>
92 </ul>
75 {childPages(thisLink, tree.children, level + 1)}
93 {childPages(thisLink, tree.children, level + 1)}
76 </div>
94 </div>
@@ -78,47 +96,6 @@ const pageTree = (thisLink, tree, level = 0) => {
78 );
96 );
79};
97};
80
98
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−
122export default function DocsLayout({ posts, data, content, params }) {
99export default function DocsLayout({ posts, data, content, params }) {
123 const [showSearch, toggleSearch] = useState(false);
100 const [showSearch, toggleSearch] = useState(false);
124 const toggle = () => toggleSearch((state) => !state);
101 const toggle = () => toggleSearch((state) => !state);
@@ -137,7 +114,7 @@ export default function DocsLayout({ posts, data, content, params }) {
137 <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
114 <div className="flex w-screen h-screen min-h-screen w-screen overflow-hidden">
138 <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
115 <Sidebar>{childPages("/docs", posts.children)}</Sidebar>
139 <ContentArea
116 <ContentArea
140− breadcrumbs={breadcrumbs(posts, params.slug.slice(0, -1))}
117+ breadcrumbs={breadcrumbs(posts, params.slug?.slice(0, -1) || "")}
141 title={data.title}
118 title={data.title}
142 toggleSearch={() => toggleSearch(true)}
119 toggleSearch={() => toggleSearch(true)}
143 >
120 >
@@ -150,15 +127,18 @@ export default function DocsLayout({ posts, data, content, params }) {
150
127
151export async function getStaticProps({ params }) {
128export async function getStaticProps({ params }) {
152 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
129 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
130+
153 const { data, content } = getPage(
131 const { data, content } = getPage(
154− join(process.cwd(), "content/docs", params.slug.join("/"))
132+ join(process.cwd(), "content/docs", params.slug?.join("/") || "/")
155 );
133 );
134+
156 return { props: { posts, data, content, params } };
135 return { props: { posts, data, content, params } };
157}
136}
158
137
159export async function getStaticPaths() {
138export async function getStaticPaths() {
160 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
139 const posts = buildPageTree(join(process.cwd(), "content/docs"), "weight");
161 const slugs = [];
140 const slugs = [];
141+
162 const allHrefs = (thisLink, tree) => {
142 const allHrefs = (thisLink, tree) => {
163 slugs.push(thisLink, ...tree.pages.map((e) => join(thisLink, e.slug)));
143 slugs.push(thisLink, ...tree.pages.map((e) => join(thisLink, e.slug)));
164 allHrefsChildren(thisLink, tree.children);
144 allHrefsChildren(thisLink, tree.children);
@@ -169,9 +149,11 @@ export async function getStaticPaths() {
169 allHrefs(join(thisLink, childSlug), child);
149 allHrefs(join(thisLink, childSlug), child);
170 });
150 });
171 };
151 };
152+
172 allHrefs("/docs", posts);
153 allHrefs("/docs", posts);
154+
173 return {
155 return {
174− paths: slugs.filter((e) => e !== "/docs"),
156+ paths: slugs,
175 fallback: false,
157 fallback: false,
176 };
158 };
177}
159}
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) {
62export default function Grants({ posts, categories, types, featuredGrants }) {
62export default function Grants({ posts, categories, types, featuredGrants }) {
63 const [activeTags, setTags] = useState([]);
63 const [activeTags, setTags] = useState([]);
64 const [activeTypes, setTypes] = useState(types);
64 const [activeTypes, setTypes] = useState(types);
65+ const [includeCompleted, setIncludeCompleted] = useState(false);
65 const [tab, setTab] = useState(0);
66 const [tab, setTab] = useState(0);
66 const [showSearch, toggleSearch] = useState(false);
67 const [showSearch, toggleSearch] = useState(false);
67
68
68− const filteredPosts = posts.reduce((acc, post) => {
69+ const filteredPosts = posts.filter((post) => {
69 // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
70 // 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)) ||
71+ const hasCategory = post.taxonomies.grant_category.some((r) =>
72+ activeTags.includes(r)
73+ );
72− activeTags.length === 0) &&
74+ const noTagsSelected = activeTags.length === 0;
73− post.taxonomies.grant_type.some((r) => activeTypes.includes(r));
75+ const hasType = post.taxonomies.grant_type.some((r) =>
74− // Return the item if the above check has passed
76+ activeTypes.includes(r)
75− if (itemDidPass === true) {
77+ );
76− return [...acc, post];
78+ const matchesCompletionFilter = includeCompleted
77− }
79+ ? true
80+ : post.extra.completed === false;
81+
78− return acc;
82+ return (
83+ (hasCategory || noTagsSelected) && hasType && matchesCompletionFilter
84+ );
79− }, []);
85+ });
80
86
81 return (
87 return (
82 <Container toggleSearch={() => toggleSearch((state) => !state)}>
88 <Container toggleSearch={() => toggleSearch((state) => !state)}>
@@ -128,12 +134,12 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
128 <div className="flex flex-col md:flex-row md:space-x-4 md:space-y-0 space-y-4 space-x-0">
134 <div className="flex flex-col md:flex-row md:space-x-4 md:space-y-0 space-y-4 space-x-0">
129 {featuredGrants.map((grant) => {
135 {featuredGrants.map((grant) => {
130 return (
136 return (
131− <Link href={grant.slug}>
137+ <Link href={"/grants/" + grant.slug}>
132 <div className="p-8 bg-washedGreen rounded-lg w-full md:w-1/3 cursor-pointer">
138 <div className="p-8 bg-washedGreen rounded-lg w-full md:w-1/3 cursor-pointer">
133 <h4 className="pb-4">{grant.title}</h4>
139 <h4 className="pb-4">{grant.title}</h4>
134 <p className="text-green pb-4">
140 <p className="text-green pb-4">
135 {grant.extra.reward} star
141 {grant.extra.reward} star
136− {grant.extra.reward > 1 ? "s" : ""}{" "}
142+ {grant.extra.reward === 1 ? "" : "s"}{" "}
137 {grant.extra.completed ? "awarded" : "pending"}
143 {grant.extra.completed ? "awarded" : "pending"}
138 </p>
144 </p>
139 <p>{grant.extra.description}</p>
145 <p>{grant.extra.description}</p>
@@ -143,70 +149,53 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
143 })}
149 })}
144 </div>
150 </div>
145 </section>
151 </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>
146 {
164 {
147 // Find a Grant
165 // Find a Grant
148 }
166 }
149 <section className="layout-wide">
167 <section className="layout-wide">
150 <h2 className="pb-8">Find A Grant</h2>
168 <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">
152 <button
170 <button
153 onClick={() => {
171 onClick={() => {
154 setTab(0);
172 setTab(0);
155 setTypes(types);
173 setTypes(types);
156 }}
174 }}
157− className={`tab-button-sm mr-4 ${
175+ className={`badge-lg mr-2 ${
158− tab === 0 ? "text-black" : "text-gray"
176+ tab === 0 ? "text-white bg-black" : "text-gray bg-wall"
159 }`}
177 }`}
160 >
178 >
161 All
179 All
162 </button>
180 </button>
163− {
181+ {types.map((type, index) => {
164− // NB Gavin: These should be auto-generated from the markdown metadata
182+ return (
165− }
183+ <button
166− <button
184+ onClick={() => {
167− onClick={() => {
185+ // + 1 is added here because the 'all' button precedes this sequence
168− setTab(1);
186+ setTab(index + 1);
169− setTypes(["Proposal"]);
187+ setTypes([type]);
170− }}
188+ }}
171− className={`tab-button-sm mr-4 ${
189+ className={`badge-lg mr-2 ${
190+ tab === index + 1
172− tab === 1 ? "text-black" : "text-gray"
191+ ? "text-white bg-black"
173− }`}
192+ : "text-gray bg-wall"
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− }`}
193+ }`}
185− >
194+ >
186− Bounties
195+ {type}
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>
196+ </button>
199− <button
197+ );
200− onClick={() => {
201− setTab(4);
202− setTypes(["Gift"]);
203− }}
204− className={`tab-button-sm ${
205− tab === 4 ? "text-black" : "text-gray"
206− }`}
198+ })}
207− >
208− Gifts
209− </button>
210 </div>
199 </div>
211
200
212 <div className="flex flex-wrap mb-12">
201 <div className="flex flex-wrap mb-12">
@@ -227,17 +216,18 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
227 })}
216 })}
228 </div>
217 </div>
229
218
230− {
231− // <div className="pb-8">
219+ <div className="pb-8 flex">
232− // <TabCarousel index={tab}>
220+ <h4>
233− // <p>All {filteredPosts.length} Urbit grants</p>
221+ Showing {filteredPosts.length} grant
234− // <p>{filteredPosts.length} proposals from community members to work on their own projects</p>
222+ {filteredPosts.length === 1 ? "" : "s"}
235− // <p>{filteredPosts.length} bounties for specific work needed by the Urbit Foundation</p>
223+ </h4>
236− // <p>{filteredPosts.length} apprenticeships for projects undertaken with a mentor</p>
224+ <button
237− // <p>{filteredPosts.length} gifts given post facto for exceptional contributions</p>
225+ className="ml-4"
238− // </TabCarousel>
226+ onClick={() => setIncludeCompleted(!includeCompleted)}
227+ >
228+ {includeCompleted ? "Exclude Completed" : "Include Completed"}
229+ </button>
239− // </div>
230+ </div>
240− }
241
231
242 {filteredPosts.map((post) => {
232 {filteredPosts.map((post) => {
243 return (
233 return (
@@ -253,9 +243,9 @@ export default function Grants({ posts, categories, types, featuredGrants }) {
253 <p className="text-gray">
243 <p className="text-gray">
254 <strong>Reward: </strong>
244 <strong>Reward: </strong>
255 {post.extra.reward} star
245 {post.extra.reward} star
256− {post.extra.reward > 1 ? "s" : ""}
246+ {post.extra.reward === 1 ? "" : "s"}
257 </p>
247 </p>
258− <div className="flex">
248+ <div className="flex items-center">
259 {post.taxonomies.grant_type.map((category) => (
249 {post.taxonomies.grant_type.map((category) => (
260 <div className="bg-black text-wall badge-sm m-1">
250 <div className="bg-black text-wall badge-sm m-1">
261 {category}
251 {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 }) {
59 // Grants
59 // Grants
60 }
60 }
61 <section className="layout-narrow">
61 <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">
63 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
63 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
64− <div>
65− <Link href="/grants">
64+ <Link href="/grants">
66− <a className="bg-green text-white badge-lg">
65+ <a className="bg-green text-white badge-lg">
67− {openGrantsCount} Open
66+ {openGrantsCount} Open
68− </a>
67+ </a>
69− </Link>
68+ </Link>
70− </div>
71 </div>
69 </div>
72 <div className="flex flex-col md:flex-row w-full items-center bg-washedGreen px-8 py-8 rounded-xl">
70 <div className="flex flex-col md:flex-row w-full items-center bg-washedGreen px-8 py-8 rounded-xl">
73 <div className="flex items-center flex-col p-4 w-full">
71 <div className="flex items-center flex-col p-4 w-full">
styles/globals.css+10−7
@@ -31,12 +31,12 @@ p a {
31
31
32h1,
32h1,
33.type-h1 {
33.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;
35}
35}
36
36
37h2,
37h2,
38.type-h2 {
38.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;
40}
40}
41
41
42h3,
42h3,
@@ -88,7 +88,7 @@ button {
88}
88}
89
89
90.badge-lg {
90.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;
92}
92}
93
93
94.tab-button-lg {
94.tab-button-lg {
@@ -152,9 +152,12 @@ a:hover {
152}
152}
153
153
154.dot::before {
154.dot::before {
155+ width: 0.5rem;
156+ height: 0.5rem;
157+ border-radius: 99px;
155− content: "·";
158+ content: "";
156 position: absolute;
159 position: absolute;
157− left: -16px;
160+ left: -1.25rem;
158− margin-top: -2px;
161+ top: 0.4rem;
159− font-size: 3rem;
162+ @apply bg-green;
160}
163}
tailwind.config.js+14
@@ -22,6 +22,20 @@ module.exports = {
22 sans: ["Inter", "-apple-system", "sans-serif"],
22 sans: ["Inter", "-apple-system", "sans-serif"],
23 mono: ["Source Code Pro", "monospace"],
23 mono: ["Source Code Pro", "monospace"],
24 },
24 },
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+ },
25 },
39 },
26 variants: {
40 variants: {
27 extend: {},
41 extend: {},