on master help sign in
Merge pull request #1485 from urbit/mp/reactive-query
99bdb21~sarlev-sarsen
package-lock.json+11
@@ -22,6 +22,7 @@
22 "gray-matter": "^4.0.3",
22 "gray-matter": "^4.0.3",
23 "html-entities": "^2.3.2",
23 "html-entities": "^2.3.2",
24 "lodash.debounce": "^4.0.8",
24 "lodash.debounce": "^4.0.8",
25+ "lodash.omit": "^4.5.0",
25 "luxon": "^2.0.2",
26 "luxon": "^2.0.2",
26 "next": "^12.1.5",
27 "next": "^12.1.5",
27 "path-browserify": "^1.0.1",
28 "path-browserify": "^1.0.1",
@@ -2078,6 +2079,11 @@
2078 "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
2079 "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
2079 "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
2080 "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
2080 },
2081 },
2082+ "node_modules/lodash.omit": {
2083+ "version": "4.5.0",
2084+ "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
2085+ "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
2086+ },
2081 "node_modules/longest-streak": {
2087 "node_modules/longest-streak": {
2082 "version": "3.0.1",
2088 "version": "3.0.1",
2083 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz",
2089 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz",
@@ -6665,6 +6671,11 @@
6665 "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
6671 "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
6666 "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
6672 "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
6667 },
6673 },
6674+ "lodash.omit": {
6675+ "version": "4.5.0",
6676+ "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
6677+ "integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA="
6678+ },
6668 "longest-streak": {
6679 "longest-streak": {
6669 "version": "3.0.1",
6680 "version": "3.0.1",
6670 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz",
6681 "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.0.1.tgz",
package.json+1
@@ -32,6 +32,7 @@
32 "gray-matter": "^4.0.3",
32 "gray-matter": "^4.0.3",
33 "html-entities": "^2.3.2",
33 "html-entities": "^2.3.2",
34 "lodash.debounce": "^4.0.8",
34 "lodash.debounce": "^4.0.8",
35+ "lodash.omit": "^4.5.0",
35 "luxon": "^2.0.2",
36 "luxon": "^2.0.2",
36 "next": "^12.1.5",
37 "next": "^12.1.5",
37 "path-browserify": "^1.0.1",
38 "path-browserify": "^1.0.1",
pages/grants.js+131−232
@@ -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";
17− getAllPosts,
18− getGrantsCategories,
13+import { getAllPosts, getGrantsCategories, getGrantsTypes } from "../lib/lib";
19− getGrantsTypes,
20− getPostBySlug,
21−} from "../lib/lib";
22
14
23−function addTag(array, tag) {
15+function isArray(arr) {
24− return [...array, tag];
16+ return Array.isArray(arr);
25}
17}
26
18
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;
31 }
22 }
32− const newArr = [...array];
23+ return isArray(query) ? query.includes(type) : type === query;
33− newArr.splice(location, 1);
34− return newArr;
35}
24}
36
25
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";
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 }
31 }
55−}
56
32
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",
38+ query: toQuery,
39+ },
40+ "",
41+ { scroll: false }
42+ );
62 }
43 }
63−}
64
44
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)
73−}) {
53+ ? null
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;
82− title: "Grants",
83− description: "Contribute to the Urbit project while earning address space.",
84− };
85−
86− let includeInProgress =
87− router.query.wip === undefined || router.query.wip === "true";
88−
89− if (
90− router.query.open === undefined &&
91− router.query.completed === undefined &&
92− router.query.wip === undefined
93− ) {
94− includeInProgress = true;
95 }
62 }
96
63
97 const annotatedPosts = posts.map((post) => {
64 const annotatedPosts = posts.map((post) => {
@@ -108,33 +75,24 @@ export default function Grants({
108
75
109 const byStatus = (post) => {
76 const byStatus = (post) => {
110 return (
77 return (
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)
114 );
81 );
115 };
82 };
116
83
117 const postsByStatus = annotatedPosts.filter(byStatus);
84 const postsByStatus = annotatedPosts.filter(byStatus);
118
85
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
88+ ? isArray(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)
123− );
91+ : true;
124
92
125 const notCanceled = !post.extra.canceled;
93 const notCanceled = !post.extra.canceled;
126−
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)
130− );
131−
132− return (
133− (hasCategory || noTagsSelected) &&
134− byStatus(post) &&
135− hasType &&
136− notCanceled
95+ return hasCategory && notCanceled && hasType;
137− );
138 });
96 });
139
97
140 const allCount = postsByStatus.length;
98 const allCount = postsByStatus.length;
@@ -155,7 +113,11 @@ export default function Grants({
155 <Container>
113 <Container>
156 <Head>
114 <Head>
157 <title>Grants • urbit.org</title>
115 <title>Grants • urbit.org</title>
158− {Meta(post)}
116+ {Meta({
117+ title: "Grants",
118+ description:
119+ "Contribute to the Urbit project while earning address space.",
120+ })}
159 </Head>
121 </Head>
160 <SingleColumn>
122 <SingleColumn>
161 <Header search={search} />
123 <Header search={search} />
@@ -318,139 +280,98 @@ export default function Grants({
318 </h2>
280 </h2>
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">
321− <button
283+ <a
322− onClick={() => {
323− let query = { ...router.query };
324− delete query.program;
325− router.push(
326− {
327− pathname: "/grants",
328− query: { ...query },
329− },
330− "",
331− { scroll: false }
332− );
333− setTypes(types);
334− }}
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"
339 }`}
286 }`}
287+ onClick={() => push((({ type, ...obj }) => obj)(router.query))}
340 >
288 >
341 All <div className="opacity-50 ml-2">{allCount}</div>
289 All <div className="opacity-50 ml-2">{allCount}</div>
342− </button>
290+ </a>
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";
302+ break;
303+ default:
304+ activeBg = "bg-black";
353− });
305+ }
354 return (
306 return (
355− <button
307+ <a
356− onClick={() => {
357− // + 1 is added here because the 'all' button precedes this sequence
358− router.push(
359− {
360− pathname: "/grants",
361− query: { ...router.query, program: type.toLowerCase() },
362− },
363− "",
364− { scroll: false }
365− );
366− setTypes([type]);
367− }}
368− className={`badge-lg mr-2 ${className}`}
308+ className={`badge-lg mr-2 ${
309+ isOrIsIn(each, type)
310+ ? activeBg
311+ : "bg-wall-100 text-wall-500"
312+ }`}
313+ onClick={() => push({ ...router.query, type: each })}
369 >
314 >
315+ {each}
370− {type} <div className="opacity-50 ml-2">{counts[type]}</div>
316+ <div className="opacity-50 ml-2">{counts[each]}</div>
371− </button>
317+ </a>
372 );
318 );
373 })}
319 })}
374 </div>
320 </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);
383 return (
328 return (
384 <button
329 <button
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)}
387 >
332 >
388− {category}
333+ {each}
389 </button>
334 </button>
390 );
335 );
391 })}
336 })}
392 </div>
337 </div>
393− {
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 },
402− },
403− "",
404− { scroll: false }
405− )
406− }
407− >
408− {includeOpen ? "Exclude Open" : "Include Open"}
344+ ? "Exclude Open"
409− </button>
345+ : "Include Open";
410−
346+ break;
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";
415− {
351+ break;
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);
422− }
358+ return (
423− >
424− {includeCompleted ? "Exclude Completed" : "Include Completed"}
425− </button>
426−
427− <button
359+ <button
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)}
430− router.push(
431− {
432− pathname: "/grants",
433− query: { ...router.query, wip: !includeInProgress },
434− },
435− "",
436− { scroll: false }
437− )
438− }
439− >
362+ >
440− {includeInProgress
363+ {title}
441− ? "Exclude In Progress"
442− : "Include In Progress"}
443− </button>
364+ </button>
444−
365+ );
366+ })}
445− <h4>
367+ <h4>
446− Showing {filteredPosts.length} grant
368+ Showing {filteredPosts.length} grant
447− {filteredPosts.length === 1 ? "" : "s"}
369+ {filteredPosts.length === 1 ? "" : "s"}
448− </h4>
370+ </h4>
449− </div>
371+ </div>
450− }
451− {filteredPosts.map((post) => {
372+ {filteredPosts.map((post) => (
452− return <GrantPreview grant={post} />;
373+ <GrantPreview grant={post} />
453− })}
374+ ))}
454 </Section>
375 </Section>
455 </SingleColumn>
376 </SingleColumn>
456 <Footer />
377 <Footer />
@@ -466,33 +387,11 @@ export async function getStaticProps() {
466 "grants",
387 "grants",
467 "date"
388 "date"
468 );
389 );
469−
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",
475− ].map((slug) =>
476− getPostBySlug(slug, ["title", "slug", "date", "extra"], "grants")
477− );
478−
479− const giftPosts = [
480− getPostBySlug(
481− "2021-06-16-update",
482− ["title", "slug", "date", "extra"],
483− "updates"
484− ),
485− getPostBySlug("gifts-q3-2020", ["title", "slug", "date", "extra"], "blog"),
486− ];
487−
488 return {
390 return {
489 props: {
391 props: {
490 posts: posts,
392 posts: posts,
491 categories,
393 categories,
492 types,
394 types,
493− featuredGrants,
494− giftPosts,
495− gifts: [],
496 },
395 },
497 };
396 };
498}
397}