@@ -1,97 +1,64 @@
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";
16−import {
12+import omit from "lodash.omit";
18− getGrantsCategories,
13+import { getAllPosts, getGrantsCategories, getGrantsTypes } from "../lib/lib";
23−function addTag(array, tag) {
15+function isArray(arr) {
24− return [...array, tag];
16+ return Array.isArray(arr);
27−function removeTag(array, tag) {
19+function isOrIsIn(type, query) {
28− const location = array.indexOf(tag);
20+ if (!query) {
29− if (location === -1) {
30− return array;
21+ return false;
32− const newArr = [...array];
23+ return isArray(query) ? query.includes(type) : type === query;
33− newArr.splice(location, 1);
37−function setAllActive(fullSet) {
26+export default function Grants({ posts, categories, types, search }) {
38− return fullSet;
27+ const router = useRouter();
39−}
28+ let { status, type, category } = router.query;
40−
29+ if (status === undefined) {
41−function setAllInactive() {
30+ status = "open";
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) {
33+ function push(toQuery) {
58− if (currentArray.length === 0) {
34+ router.push(
59− return setAllActive(fullSet);
35+ {
60− } else {
36+ pathname: "/grants",
61− return setAllInactive();
37+ hash: "view-grants",
65−export default function Grants({
45+ function pushOrDropQuery(queryName, currentQuery, item) {
66− posts,
46+ let to, toWithQuery;
67− categories,
47+ if (isArray(currentQuery)) {
68− types,
48+ to = isOrIsIn(item, currentQuery)
69− featuredGrants,
49+ ? currentQuery.filter((e) => e !== item)
70− search,
50+ : [...currentQuery, item];
71− giftPosts,
51+ } else {
72− gifts,
52+ to = isOrIsIn(item, currentQuery)
74− const router = useRouter();
54+ : [currentQuery, item].filter((e) => e !== undefined);
75− const [activeTags, setTags] = useState([]);
55+ }
76− const [activeTypes, setTypes] = useState(types);
56+ if (!to) {
77− const includeOpen =
57+ toWithQuery = omit(router.query, queryName);
78− router.query.open === undefined || router.query.open === "true";
58+ } else {
79− const includeCompleted = router.query.completed === "true";
59+ toWithQuery = { ...router.query, [queryName]: to };
80− const programFilter = router.query.program;
60+ }
81− const post = {
61+ return toWithQuery;
83− description: "Contribute to the Urbit project while earning address space.",
86− let includeInProgress =
87− router.query.wip === undefined || router.query.wip === "true";
90− router.query.open === undefined &&
91− router.query.completed === undefined &&
92− router.query.wip === undefined
94− includeInProgress = true;
97 const annotatedPosts = posts.map((post) => {
64 const annotatedPosts = posts.map((post) => {
@@ -108,33 +75,24 @@ export default function Grants({
109 const byStatus = (post) => {
76 const byStatus = (post) => {
111− (includeOpen ? post.status === "open" : false) ||
78+ (isOrIsIn("open", status) ? post.status === "open" : false) ||
112− (includeCompleted ? post.status === "completed" : false) ||
79+ (isOrIsIn("complete", status) ? post.status === "completed" : false) ||
113− (includeInProgress ? post.status === "wip" : false)
80+ (isOrIsIn("wip", status) ? post.status === "wip" : false)
117 const postsByStatus = annotatedPosts.filter(byStatus);
84 const postsByStatus = annotatedPosts.filter(byStatus);
119− const filteredPosts = annotatedPosts.filter((post) => {
86+ const filteredPosts = postsByStatus.filter((post) => {
120− // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
87+ const hasCategory = category
121− const hasCategory = post.taxonomies.grant_category.some((category) =>
89+ ? post.taxonomies.grant_category.some((cat) => category.includes(cat))
122− activeTags.includes(category)
90+ : post.taxonomies.grant_category.includes(category)
125 const notCanceled = !post.extra.canceled;
93 const notCanceled = !post.extra.canceled;
127− const noTagsSelected = activeTags.length === 0;
128− const hasType = post.taxonomies.grant_type.some((type) =>
94+ const hasType = type ? post.taxonomies.grant_type.includes(type) : true;
129− activeTypes.includes(type)
133− (hasCategory || noTagsSelected) &&
136− notCanceled
95+ return hasCategory && notCanceled && hasType;
140 const allCount = postsByStatus.length;
98 const allCount = postsByStatus.length;
@@ -155,7 +113,11 @@ export default function Grants({
155 <Container>
113 <Container>
157 <title>Grants • urbit.org</title>
115 <title>Grants • urbit.org</title>
158− {Meta(post)}
116+ {Meta({
119+ "Contribute to the Urbit project while earning address space.",
160 <SingleColumn>
122 <SingleColumn>
161 <Header search={search} />
123 <Header search={search} />
@@ -318,139 +280,98 @@ export default function Grants({
319 <h5 className="text-wall-600 font-semibold my-2">Programs</h5>
281 <h5 className="text-wall-600 font-semibold my-2">Programs</h5>
320 <div className="flex flex-wrap items-center pb-2">
282 <div className="flex flex-wrap items-center pb-2">
323− let query = { ...router.query };
324− delete query.program;
327− pathname: "/grants",
328− query: { ...query },
335− className={`badge-lg my-2 mr-2 ${
284+ className={`badge-lg mr-2 ${
336− programFilter === undefined
337− ? "text-white bg-black"
338− : "text-wall-500 bg-wall-100"
285+ !type ? "bg-black text-white" : "bg-wall-100 text-wall-500"
287+ onClick={() => push((({ type, ...obj }) => obj)(router.query))}
341 All <div className="opacity-50 ml-2">{allCount}</div>
289 All <div className="opacity-50 ml-2">{allCount}</div>
343− {types.map((type) => {
291+ {types.map((each) => {
344− const className = classnames({
292+ let activeBg;
345− "bg-blue-400 text-white":
293+ switch (each) {
346− programFilter === "proposal" && type === "Proposal",
294+ case "Proposal":
347− "bg-green-400 text-white":
295+ activeBg = "bg-blue-400 text-white";
348− programFilter === "apprenticeship" &&
296+ break;
349− type === "Apprenticeship",
297+ case "Bounty":
350− "bg-yellow-300":
298+ activeBg = "bg-yellow-300 text-white";
351− programFilter === "bounty" && type === "Bounty",
299+ break;
352− "bg-wall-100 text-wall-500": type !== programFilter,
300+ case "Apprenticeship":
301+ activeBg = "bg-green-400 text-white";
304+ activeBg = "bg-black";
357− // + 1 is added here because the 'all' button precedes this sequence
360− pathname: "/grants",
361− query: { ...router.query, program: type.toLowerCase() },
368− className={`badge-lg mr-2 ${className}`}
308+ className={`badge-lg mr-2 ${
309+ isOrIsIn(each, type)
311+ : "bg-wall-100 text-wall-500"
313+ onClick={() => push({ ...router.query, type: each })}
370− {type} <div className="opacity-50 ml-2">{counts[type]}</div>
316+ <div className="opacity-50 ml-2">{counts[each]}</div>
375 <h5 className="text-wall-600 font-semibold my-2">Work Categories</h5>
321 <h5 className="text-wall-600 font-semibold my-2">Work Categories</h5>
376 <div className="flex flex-wrap mb-12">
322 <div className="flex flex-wrap mb-12">
377− {categories.map((category) => {
323+ {categories.map((each) => {
378− const isActive = activeTags.includes(category);
324+ const active = isOrIsIn(each, category)
379− const activeClasses = classnames({
380− "bg-green-400 text-white": isActive,
325+ ? "bg-green-400 text-white"
381− "bg-wall-100 text-wall-500": !isActive,
326+ : "bg-wall-100 text-wall-500";
382− });
327+ const categoryQuery = pushOrDropQuery("category", category, each);
385− onClick={() => setTags(toggleTag(activeTags, category))}
386− className={`${activeClasses} badge-sm mr-1 my-1`}
330+ className={`${active} badge-sm mr-1 my-1`}
331+ onClick={() => push(categoryQuery)}
388− {category}
333+ {each}
389 </button>
334 </button>
394− <div className="pb-8 flex items-center">
338+ <div className="pb-8 flex items-center">
395− <button
339+ {["open", "complete", "wip"].map((each) => {
396− className="mr-4 badge-sm bg-black text-white"
340+ let title;
397− onClick={() =>
341+ switch (each) {
398− router.push(
342+ case "open":
399− {
343+ title = isOrIsIn(each, status)
400− pathname: "/grants",
401− query: { ...router.query, open: !includeOpen },
408− {includeOpen ? "Exclude Open" : "Include Open"}
344+ ? "Exclude Open"
409− </button>
345+ : "Include Open";
411− <button
347+ case "complete":
412− className="mr-4 badge-sm bg-black text-white"
348+ title = isOrIsIn(each, status)
413− onClick={() =>
349+ ? "Exclude Completed"
414− router.push(
350+ : "Include Completed";
416− pathname: "/grants",
352+ case "wip":
417− query: { ...router.query, completed: !includeCompleted },
353+ title = isOrIsIn(each, status)
418− },
354+ ? "Exclude In Progress"
419− "",
355+ : "Include In Progress";
420− { scroll: false }
356+ }
421− )
357+ const statusQuery = pushOrDropQuery("status", status, each);
424− {includeCompleted ? "Exclude Completed" : "Include Completed"}
428− className="mr-4 badge-sm bg-black text-white"
360+ className="mr-4 badge-sm bg-black text-white"
429− onClick={() =>
361+ onClick={() => push(statusQuery)}
432− pathname: "/grants",
433− query: { ...router.query, wip: !includeInProgress },
440− {includeInProgress
363+ {title}
441− ? "Exclude In Progress"
442− : "Include In Progress"}
443− </button>
364+ </button>
446− Showing {filteredPosts.length} grant
368+ Showing {filteredPosts.length} grant
447− {filteredPosts.length === 1 ? "" : "s"}
369+ {filteredPosts.length === 1 ? "" : "s"}
451− {filteredPosts.map((post) => {
372+ {filteredPosts.map((post) => (
452− return <GrantPreview grant={post} />;
373+ <GrantPreview grant={post} />
454 </Section>
375 </Section>
455 </SingleColumn>
376 </SingleColumn>
456 <Footer />
377 <Footer />
@@ -466,33 +387,11 @@ export async function getStaticProps() {
466 "grants",
387 "grants",
470− // The layout expects exactly 3
471− const featuredGrants = [
472− "bitcoin-full-node-provider-and-wallet",
473− "webrtc-gall-agent-and-external-app",
474− "urbian-a-customized-linux-distribution-for-urbit-appliances",
476− getPostBySlug(slug, ["title", "slug", "date", "extra"], "grants")
481− "2021-06-16-update",
482− ["title", "slug", "date", "extra"],
485− getPostBySlug("gifts-q3-2020", ["title", "slug", "date", "extra"], "blog"),
490 posts: posts,
392 posts: posts,
491 categories,
393 categories,