@@ -1,16 +1,12 @@
1import Head from "next/head";
1import Head from "next/head";
2−import { TableOfContents } from "../components/TableOfContents";
3import Meta from "../components/Meta";
2import Meta from "../components/Meta";
4import Link from "next/link";
3import Link from "next/link";
5import { useRouter } from "next/router";
4import { useRouter } from "next/router";
6−import { useState } from "react";
7−import classnames from "classnames";
8import Container from "../components/Container";
5import Container from "../components/Container";
9import Footer from "../components/Footer";
6import Footer from "../components/Footer";
10import Header from "../components/Header";
7import Header from "../components/Header";
11import SingleColumn from "../components/SingleColumn";
8import SingleColumn from "../components/SingleColumn";
12import Section from "../components/Section";
9import Section from "../components/Section";
13−import PostPreview from "../components/PostPreview";
14import GrantPreview from "../components/GrantPreview";
10import GrantPreview from "../components/GrantPreview";
15import JoinGroup from "../components/JoinGroup";
11import JoinGroup from "../components/JoinGroup";
@@ -20,73 +16,32 @@ import {
20 getPostBySlug,
16 getPostBySlug,
21} from "../lib/lib";
17} from "../lib/lib";
23−function addTag(array, tag) {
19+function isArray(arr) {
24− return [...array, tag];
20+ return Array.isArray(arr);
27−function removeTag(array, tag) {
23+function isOrIsIn(type, query) {
28− const location = array.indexOf(tag);
24+ if (!query) {
29− if (location === -1) {
30− return array;
25+ return false;
32− const newArr = [...array];
27+ return isArray(query) ? query.includes(type) : type === query;
33− newArr.splice(location, 1);
37−function setAllActive(fullSet) {
41−function setAllInactive() {
45−function setOneTagOnly(tag) {
49−function toggleTag(array, tag) {
50− if (array.includes(tag)) {
51− return removeTag(array, tag);
53− return addTag(array, tag);
57−function toggleAll(currentArray, fullSet) {
58− if (currentArray.length === 0) {
59− return setAllActive(fullSet);
61− return setAllInactive();
65−export default function Grants({
30+export default function Grants({ posts, categories, types, search }) {
74 const router = useRouter();
31 const router = useRouter();
75− const { open, completed, program, wip } = router.query;
32+ let { status, type, category } = router.query;
76− const [activeTags, setTags] = useState([]);
33+ if (status === undefined) {
77− const [activeTypes, setTypes] = useState(types);
34+ status = "open";
78− const includeOpen = open === "true";
35+ }
79− const includeCompleted = completed === "true";
36+ function push(toQuery) {
80− const programFilter = program;
37+ router.push(
82− title: "Grants",
39+ pathname: "/grants",
83− description: "Contribute to the Urbit project while earning address space.",
40+ query: toQuery,
86− let includeInProgress = wip === undefined || wip === "true";
43+ { scroll: false }
88− if (open === undefined && completed === undefined && wip === undefined) {
89− includeInProgress = true;
92 const annotatedPosts = posts.map((post) => {
47 const annotatedPosts = posts.map((post) => {
@@ -103,33 +58,24 @@ export default function Grants({
104 const byStatus = (post) => {
59 const byStatus = (post) => {
106− (includeOpen ? post.status === "open" : false) ||
61+ (isOrIsIn("open", status) ? post.status === "open" : false) ||
107− (includeCompleted ? post.status === "completed" : false) ||
62+ (isOrIsIn("complete", status) ? post.status === "completed" : false) ||
108− (includeInProgress ? post.status === "wip" : false)
63+ (isOrIsIn("wip", status) ? post.status === "wip" : false)
112 const postsByStatus = annotatedPosts.filter(byStatus);
67 const postsByStatus = annotatedPosts.filter(byStatus);
114− const filteredPosts = annotatedPosts.filter((post) => {
69+ const filteredPosts = postsByStatus.filter((post) => {
115− // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
70+ const hasCategory = category
116− const hasCategory = post.taxonomies.grant_category.some((category) =>
72+ ? post.taxonomies.grant_category.some((cat) => category.includes(cat))
117− activeTags.includes(category)
73+ : post.taxonomies.grant_category.includes(category)
120 const notCanceled = !post.extra.canceled;
76 const notCanceled = !post.extra.canceled;
122− const noTagsSelected = activeTags.length === 0;
123− const hasType = post.taxonomies.grant_type.some((type) =>
77+ const hasType = type ? post.taxonomies.grant_type.includes(type) : true;
124− activeTypes.includes(type)
128− (hasCategory || noTagsSelected) &&
131− notCanceled
78+ return hasCategory && notCanceled && hasType;
135 const allCount = postsByStatus.length;
81 const allCount = postsByStatus.length;
@@ -150,7 +96,11 @@ export default function Grants({
150 <Container>
96 <Container>
152 <title>Grants • urbit.org</title>
98 <title>Grants • urbit.org</title>
153− {Meta(post)}
99+ {Meta({
102+ "Contribute to the Urbit project while earning address space.",
155 <SingleColumn>
105 <SingleColumn>
156 <Header search={search} />
106 <Header search={search} />
@@ -313,139 +263,124 @@ export default function Grants({
314 <h5 className="text-wall-600 font-semibold my-2">Programs</h5>
264 <h5 className="text-wall-600 font-semibold my-2">Programs</h5>
315 <div className="flex flex-wrap items-center pb-2">
265 <div className="flex flex-wrap items-center pb-2">
318− let query = { ...router.query };
319− delete query.program;
322− pathname: "/grants",
323− query: { ...query },
330− className={`badge-lg my-2 mr-2 ${
267+ className={`badge-lg mr-2 ${
331− programFilter === undefined
332− ? "text-white bg-black"
333− : "text-wall-500 bg-wall-100"
268+ !type ? "bg-black text-white" : "bg-wall-100 text-wall-500"
270+ onClick={() => push((({ type, ...obj }) => obj)(router.query))}
336 All <div className="opacity-50 ml-2">{allCount}</div>
272 All <div className="opacity-50 ml-2">{allCount}</div>
338− {types.map((type) => {
274+ {types.map((each) => {
339− const className = classnames({
275+ let activeBg;
340− "bg-blue-400 text-white":
276+ switch (each) {
341− programFilter === "proposal" && type === "Proposal",
277+ case "Proposal":
342− "bg-green-400 text-white":
278+ activeBg = "bg-blue-400 text-white";
343− programFilter === "apprenticeship" &&
279+ break;
344− type === "Apprenticeship",
280+ case "Bounty":
345− "bg-yellow-300":
281+ activeBg = "bg-yellow-300 text-white";
346− programFilter === "bounty" && type === "Bounty",
282+ break;
347− "bg-wall-100 text-wall-500": type !== programFilter,
283+ case "Apprenticeship":
284+ activeBg = "bg-green-400 text-white";
287+ activeBg = "bg-black";
352− // + 1 is added here because the 'all' button precedes this sequence
355− pathname: "/grants",
356− query: { ...router.query, program: type.toLowerCase() },
363− className={`badge-lg mr-2 ${className}`}
291+ className={`badge-lg mr-2 ${
292+ isOrIsIn(each, type)
294+ : "bg-wall-100 text-wall-500"
296+ onClick={() => push({ ...router.query, type: each })}
365− {type} <div className="opacity-50 ml-2">{counts[type]}</div>
299+ <div className="opacity-50 ml-2">{counts[each]}</div>
370 <h5 className="text-wall-600 font-semibold my-2">Work Categories</h5>
304 <h5 className="text-wall-600 font-semibold my-2">Work Categories</h5>
371 <div className="flex flex-wrap mb-12">
305 <div className="flex flex-wrap mb-12">
372− {categories.map((category) => {
306+ {categories.map((each) => {
373− const isActive = activeTags.includes(category);
307+ const active = isOrIsIn(each, category)
374− const activeClasses = classnames({
375− "bg-green-400 text-white": isActive,
308+ ? "bg-green-400 text-white"
376− "bg-wall-100 text-wall-500": !isActive,
309+ : "bg-wall-100 text-wall-500";
377− });
310+ let to, categoryQuery;
311+ if (isArray(category)) {
312+ to = isOrIsIn(each, category)
313+ ? category.filter((e) => e !== each)
314+ : [...category, each];
316+ to = isOrIsIn(each, category)
318+ : [category, each].filter((e) => e !== undefined);
321+ categoryQuery = (({ category, ...obj }) => obj)(router.query);
323+ categoryQuery = { ...router.query, category: to };
380− onClick={() => setTags(toggleTag(activeTags, category))}
381− className={`${activeClasses} badge-sm mr-1 my-1`}
327+ className={`${active} badge-sm mr-1 my-1`}
328+ onClick={() => push(categoryQuery)}
383− {category}
330+ {each}
384 </button>
331 </button>
389− <div className="pb-8 flex items-center">
335+ <div className="pb-8 flex items-center">
390− <button
336+ {["open", "complete", "wip"].map((each) => {
391− className="mr-4 badge-sm bg-black text-white"
337+ let title;
392− onClick={() =>
338+ switch (each) {
393− router.push(
339+ case "open":
394− {
340+ title = isOrIsIn(each, status)
395− pathname: "/grants",
341+ ? "Exclude Open"
396− query: { ...router.query, open: !includeOpen },
342+ : "Include Open";
398− "",
344+ case "complete":
399− { scroll: false }
345+ title = isOrIsIn(each, status)
400− )
346+ ? "Exclude Completed"
401− }
347+ : "Include Completed";
403− {includeOpen ? "Exclude Open" : "Include Open"}
349+ case "wip":
404− </button>
350+ title = isOrIsIn(each, status)
405−
351+ ? "Exclude In Progress"
406− <button
352+ : "Include In Progress";
407− className="mr-4 badge-sm bg-black text-white"
353+ }
408− onClick={() =>
354+ let to, statusQuery;
409− router.push(
355+ if (isArray(status)) {
410− {
356+ to = isOrIsIn(each, status)
411− pathname: "/grants",
357+ ? status.filter((e) => e !== each)
412− query: { ...router.query, completed: !includeCompleted },
358+ : [...status, each];
360+ to = isOrIsIn(each, status) ? null : [status, each];
363+ statusQuery = (({ status, ...obj }) => obj)(router.query);
365+ statusQuery = { ...router.query, status: to };
419− {includeCompleted ? "Exclude Completed" : "Include Completed"}
423− className="mr-4 badge-sm bg-black text-white"
369+ className="mr-4 badge-sm bg-black text-white"
424− onClick={() =>
370+ onClick={() => push(statusQuery)}
427− pathname: "/grants",
428− query: { ...router.query, wip: !includeInProgress },
435− {includeInProgress
372+ {title}
436− ? "Exclude In Progress"
437− : "Include In Progress"}
438− </button>
373+ </button>
441− Showing {filteredPosts.length} grant
377+ Showing {filteredPosts.length} grant
442− {filteredPosts.length === 1 ? "" : "s"}
378+ {filteredPosts.length === 1 ? "" : "s"}
446− {filteredPosts.map((post) => {
381+ {filteredPosts.map((post) => (
447− return <GrantPreview grant={post} />;
382+ <GrantPreview grant={post} />
449 </Section>
384 </Section>
450 </SingleColumn>
385 </SingleColumn>
451 <Footer />
386 <Footer />
@@ -461,33 +396,11 @@ export async function getStaticProps() {
461 "grants",
396 "grants",
465− // The layout expects exactly 3
466− const featuredGrants = [
467− "bitcoin-full-node-provider-and-wallet",
468− "webrtc-gall-agent-and-external-app",
469− "urbian-a-customized-linux-distribution-for-urbit-appliances",
471− getPostBySlug(slug, ["title", "slug", "date", "extra"], "grants")
476− "2021-06-16-update",
477− ["title", "slug", "date", "extra"],
480− getPostBySlug("gifts-q3-2020", ["title", "slug", "date", "extra"], "blog"),
485 posts: posts,
401 posts: posts,
486 categories,
402 categories,