on master help sign in
grants: rewrite everything to only use queries
cedf6d4Matilde Park
pages/grants.js+137−224
@@ -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";
16import {
12import {
@@ -20,73 +16,32 @@ import {
20 getPostBySlug,
16 getPostBySlug,
21} from "../lib/lib";
17} from "../lib/lib";
22
18
23−function addTag(array, tag) {
19+function isArray(arr) {
24− return [...array, tag];
20+ return Array.isArray(arr);
25}
21}
26
22
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;
31 }
26 }
32− const newArr = [...array];
27+ return isArray(query) ? query.includes(type) : type === query;
33− newArr.splice(location, 1);
34− return newArr;
35}
28}
36
29
37−function setAllActive(fullSet) {
38− return fullSet;
39−}
40−
41−function setAllInactive() {
42− return [];
43−}
44−
45−function setOneTagOnly(tag) {
46− return [tag];
47−}
48−
49−function toggleTag(array, tag) {
50− if (array.includes(tag)) {
51− return removeTag(array, tag);
52− } else {
53− return addTag(array, tag);
54− }
55−}
56−
57−function toggleAll(currentArray, fullSet) {
58− if (currentArray.length === 0) {
59− return setAllActive(fullSet);
60− } else {
61− return setAllInactive();
62− }
63−}
64−
65−export default function Grants({
30+export default function Grants({ posts, categories, types, search }) {
66− posts,
67− categories,
68− types,
69− featuredGrants,
70− search,
71− giftPosts,
72− gifts,
73−}) {
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(
81− const post = {
38+ {
82− title: "Grants",
39+ pathname: "/grants",
83− description: "Contribute to the Urbit project while earning address space.",
40+ query: toQuery,
84− };
41+ },
85−
42+ "",
86− let includeInProgress = wip === undefined || wip === "true";
43+ { scroll: false }
87−
44+ );
88− if (open === undefined && completed === undefined && wip === undefined) {
89− includeInProgress = true;
90 }
45 }
91
46
92 const annotatedPosts = posts.map((post) => {
47 const annotatedPosts = posts.map((post) => {
@@ -103,33 +58,24 @@ export default function Grants({
103
58
104 const byStatus = (post) => {
59 const byStatus = (post) => {
105 return (
60 return (
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)
109 );
64 );
110 };
65 };
111
66
112 const postsByStatus = annotatedPosts.filter(byStatus);
67 const postsByStatus = annotatedPosts.filter(byStatus);
113
68
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
71+ ? isArray(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)
118− );
74+ : true;
119
75
120 const notCanceled = !post.extra.canceled;
76 const notCanceled = !post.extra.canceled;
121−
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)
125− );
126−
127− return (
128− (hasCategory || noTagsSelected) &&
129− byStatus(post) &&
130− hasType &&
131− notCanceled
78+ return hasCategory && notCanceled && hasType;
132− );
133 });
79 });
134
80
135 const allCount = postsByStatus.length;
81 const allCount = postsByStatus.length;
@@ -150,7 +96,11 @@ export default function Grants({
150 <Container>
96 <Container>
151 <Head>
97 <Head>
152 <title>Grants • urbit.org</title>
98 <title>Grants • urbit.org</title>
153− {Meta(post)}
99+ {Meta({
100+ title: "Grants",
101+ description:
102+ "Contribute to the Urbit project while earning address space.",
103+ })}
154 </Head>
104 </Head>
155 <SingleColumn>
105 <SingleColumn>
156 <Header search={search} />
106 <Header search={search} />
@@ -313,139 +263,124 @@ export default function Grants({
313 </h2>
263 </h2>
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">
316− <button
266+ <a
317− onClick={() => {
318− let query = { ...router.query };
319− delete query.program;
320− router.push(
321− {
322− pathname: "/grants",
323− query: { ...query },
324− },
325− "",
326− { scroll: false }
327− );
328− setTypes(types);
329− }}
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"
334 }`}
269 }`}
270+ onClick={() => push((({ type, ...obj }) => obj)(router.query))}
335 >
271 >
336 All <div className="opacity-50 ml-2">{allCount}</div>
272 All <div className="opacity-50 ml-2">{allCount}</div>
337− </button>
273+ </a>
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";
285+ break;
286+ default:
287+ activeBg = "bg-black";
348− });
288+ }
349 return (
289 return (
350− <button
290+ <a
351− onClick={() => {
352− // + 1 is added here because the 'all' button precedes this sequence
353− router.push(
354− {
355− pathname: "/grants",
356− query: { ...router.query, program: type.toLowerCase() },
357− },
358− "",
359− { scroll: false }
360− );
361− setTypes([type]);
362− }}
363− className={`badge-lg mr-2 ${className}`}
291+ className={`badge-lg mr-2 ${
292+ isOrIsIn(each, type)
293+ ? activeBg
294+ : "bg-wall-100 text-wall-500"
295+ }`}
296+ onClick={() => push({ ...router.query, type: each })}
364 >
297 >
298+ {each}
365− {type} <div className="opacity-50 ml-2">{counts[type]}</div>
299+ <div className="opacity-50 ml-2">{counts[each]}</div>
366− </button>
300+ </a>
367 );
301 );
368 })}
302 })}
369 </div>
303 </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];
315+ } else {
316+ to = isOrIsIn(each, category)
317+ ? null
318+ : [category, each].filter((e) => e !== undefined);
319+ }
320+ if (!to) {
321+ categoryQuery = (({ category, ...obj }) => obj)(router.query);
322+ } else {
323+ categoryQuery = { ...router.query, category: to };
324+ }
378 return (
325 return (
379 <button
326 <button
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)}
382 >
329 >
383− {category}
330+ {each}
384 </button>
331 </button>
385 );
332 );
386 })}
333 })}
387 </div>
334 </div>
388− {
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";
397− },
343+ break;
398− "",
344+ case "complete":
399− { scroll: false }
345+ title = isOrIsIn(each, status)
400− )
346+ ? "Exclude Completed"
401− }
347+ : "Include Completed";
402− >
348+ break;
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];
359+ } else {
360+ to = isOrIsIn(each, status) ? null : [status, each];
361+ }
362+ if (!to) {
363+ statusQuery = (({ status, ...obj }) => obj)(router.query);
364+ } else {
365+ statusQuery = { ...router.query, status: to };
413− },
366+ }
414− "",
367+ return (
415− { scroll: false }
416− )
417− }
418− >
419− {includeCompleted ? "Exclude Completed" : "Include Completed"}
420− </button>
421−
422− <button
368+ <button
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)}
425− router.push(
426− {
427− pathname: "/grants",
428− query: { ...router.query, wip: !includeInProgress },
429− },
430− "",
431− { scroll: false }
432− )
433− }
434− >
371+ >
435− {includeInProgress
372+ {title}
436− ? "Exclude In Progress"
437− : "Include In Progress"}
438− </button>
373+ </button>
439−
374+ );
375+ })}
440− <h4>
376+ <h4>
441− Showing {filteredPosts.length} grant
377+ Showing {filteredPosts.length} grant
442− {filteredPosts.length === 1 ? "" : "s"}
378+ {filteredPosts.length === 1 ? "" : "s"}
443− </h4>
379+ </h4>
444− </div>
380+ </div>
445− }
446− {filteredPosts.map((post) => {
381+ {filteredPosts.map((post) => (
447− return <GrantPreview grant={post} />;
382+ <GrantPreview grant={post} />
448− })}
383+ ))}
449 </Section>
384 </Section>
450 </SingleColumn>
385 </SingleColumn>
451 <Footer />
386 <Footer />
@@ -461,33 +396,11 @@ export async function getStaticProps() {
461 "grants",
396 "grants",
462 "date"
397 "date"
463 );
398 );
464−
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",
470− ].map((slug) =>
471− getPostBySlug(slug, ["title", "slug", "date", "extra"], "grants")
472− );
473−
474− const giftPosts = [
475− getPostBySlug(
476− "2021-06-16-update",
477− ["title", "slug", "date", "extra"],
478− "updates"
479− ),
480− getPostBySlug("gifts-q3-2020", ["title", "slug", "date", "extra"], "blog"),
481− ];
482−
483 return {
399 return {
484 props: {
400 props: {
485 posts: posts,
401 posts: posts,
486 categories,
402 categories,
487 types,
403 types,
488− featuredGrants,
489− giftPosts,
490− gifts: [],
491 },
404 },
492 };
405 };
493}
406}