on master help sign in
sorting interface for grants
692aea3Gavin Atkinson
content/grants/apprenticeships/_index.mddeleted−4
@@ -1,4 +0,0 @@
1−+++
2−title = "Apprenticeships"
3−template = "grant_type/apprenticeships.html"
4−+++
content/grants/bounties/_index.mddeleted−4
@@ -1,4 +0,0 @@
1−+++
2−title = "Bounties"
3−template = "grant_type/bounties.html"
4−+++
content/grants/gifts/_index.mddeleted−4
@@ -1,4 +0,0 @@
1−+++
2−title = "Gifts"
3−template = "grant_type/gifts.html"
4−+++
content/grants/proposals/_index.mddeleted−4
@@ -1,4 +0,0 @@
1−+++
2−title = "Proposals"
3−template = "grant_type/proposals.html"
4−+++
lib/lib.js+5−14
@@ -228,15 +228,15 @@ export function getGrantsTypes() {
228 const uniqueTypes = paths
228 const uniqueTypes = paths
229 // Get only markdown files
229 // Get only markdown files
230 .filter((path) => parse(path).ext === ".md")
230 .filter((path) => parse(path).ext === ".md")
231− // Parse frontmatter
231+ // Parse frontmatter to JSON
232 .map((path) =>
232 .map((path) =>
233 matter(fs.readFileSync(join(directories.grants, path)), options)
233 matter(fs.readFileSync(join(directories.grants, path)), options)
234 )
234 )
235− // Each markdown file has an array of categories. Collapse these arrays into one array.
235+ // Each markdown file has an array of types. Collapse these arrays into one array.
236 .reduce((acc, metadata) => {
236 .reduce((acc, metadata) => {
237 return [...acc, ...metadata.data.taxonomies.grant_type];
237 return [...acc, ...metadata.data.taxonomies.grant_type];
238 }, [])
238 }, [])
239− // Deduplicate single array of categories such that the output is an array where each category is represented only once.
239+ // Deduplicate single array of types such that the output is an array where each type is represented only once.
240 .reduce((acc, uniqueItem) => {
240 .reduce((acc, uniqueItem) => {
241 if (acc.includes(uniqueItem)) {
241 if (acc.includes(uniqueItem)) {
242 return acc;
242 return acc;
@@ -246,12 +246,13 @@ export function getGrantsTypes() {
246 return uniqueTypes;
246 return uniqueTypes;
247}
247}
248
248
249+// Returns a number which represents how many grants are open, or uncompleted
249export function getOpenGrantsCount() {
250export function getOpenGrantsCount() {
250 const paths = fs.readdirSync(directories.grants);
251 const paths = fs.readdirSync(directories.grants);
251 const count = paths
252 const count = paths
252 // Get only markdown files
253 // Get only markdown files
253 .filter((path) => parse(path).ext === ".md")
254 .filter((path) => parse(path).ext === ".md")
254− // Parse frontmatter
255+ // Parse frontmatter to JSON
255 .map((path) =>
256 .map((path) =>
256 matter(fs.readFileSync(join(directories.grants, path)), options)
257 matter(fs.readFileSync(join(directories.grants, path)), options)
257 )
258 )
@@ -264,13 +265,3 @@ export function getOpenGrantsCount() {
264 }, 0);
265 }, 0);
265 return count;
266 return count;
266}
267}
267−
268−/*
269− * types = an array of strings
270− */
271−export function getGrantsByCategories(types) {}
272−
273−/*
274− * types = an array of strings
275− */
276−export function getGrantsByTypes(types) {}
pages/events.js−1
@@ -69,7 +69,6 @@ function EventCard(props) {
69}
69}
70
70
71export default function Events({ pastEvents, currentEvents }) {
71export default function Events({ pastEvents, currentEvents }) {
72− console.log(currentEvents);
73 return (
72 return (
74 <Container>
73 <Container>
75 <SingleColumn>
74 <SingleColumn>
pages/grants.js+173−49
@@ -1,11 +1,13 @@
1import Head from "next/head";
1import Head from "next/head";
2import Link from "next/link";
2import Link from "next/link";
3import { useState } from "react";
3import { useState } from "react";
4+import classnames from "classnames";
4import Container from "../components/Container";
5import Container from "../components/Container";
5import Footer from "../components/Footer";
6import Footer from "../components/Footer";
6import Header from "../components/Header";
7import Header from "../components/Header";
7import SingleColumn from "../components/SingleColumn";
8import SingleColumn from "../components/SingleColumn";
8import BackgroundImage from "../components/BackgroundImage";
9import BackgroundImage from "../components/BackgroundImage";
10+import TabCarousel from "../components/TabCarousel";
9import {
11import {
10 getAllPosts,
12 getAllPosts,
11 formatDate,
13 formatDate,
@@ -13,7 +15,53 @@ import {
13 getGrantsTypes,
15 getGrantsTypes,
14} from "../lib/lib";
16} from "../lib/lib";
15
17
18+function addTag(array, tag) {
19+ return [...array, tag];
20+}
21+
22+function removeTag(array, tag) {
23+ const location = array.indexOf(tag);
24+ if (location === -1) {
25+ return array;
26+ }
27+ const newArr = [...array];
28+ newArr.splice(location, 1);
29+ return newArr;
30+}
31+
32+function setAllActive(fullSet) {
33+ return fullSet;
34+}
35+
36+function setAllInactive() {
37+ return [];
38+}
39+
40+function setOneTagOnly(tag) {
41+ return [tag];
42+}
43+
44+function toggleTag(array, tag) {
45+ if (array.includes(tag)) {
46+ return removeTag(array, tag);
47+ } else {
48+ return addTag(array, tag);
49+ }
50+}
51+
52+function toggleAll(currentArray, fullSet) {
53+ if (currentArray.length === 0) {
54+ return setAllActive(fullSet);
55+ } else {
56+ return setAllInactive();
57+ }
58+}
59+
16−export default function Grants({ posts }) {
60+export default function Grants({ posts, categories, types }) {
61+ const [activeTags, setTags] = useState([]);
62+ const [activeTypes, setTypes] = useState(types);
63+ const [tab, setTab] = useState(0);
64+
17 return (
65 return (
18 <Container>
66 <Container>
19 <SingleColumn>
67 <SingleColumn>
@@ -21,55 +69,134 @@ export default function Grants({ posts }) {
21 <section className="layout-wide">
69 <section className="layout-wide">
22 <div className="measure">
70 <div className="measure">
23 <h2 className="pb-16">Urbit Grants</h2>
71 <h2 className="pb-16">Urbit Grants</h2>
24− <p className="pb-6">
72+ <p className="">
25 Urbit is a community project. While anyone can contribute, we help
73 Urbit is a community project. While anyone can contribute, we help
26 focus development and reward exceptional contribution through our
74 focus development and reward exceptional contribution through our
27 grants program.
75 grants program.
28 </p>
76 </p>
29− <p>We reward contributors in four different ways:</p>
30 </div>
77 </div>
31 </section>
78 </section>
32 <section className="layout-wide">
79 <section className="layout-wide">
33− {posts.map((post) => {
34− return (
35− <div key={post.slug} className="mb-24 cursor-pointer">
36− <Link href={`/grants/${post.slug}`}>
37− <div>
38− {
39− // Not all blog posts have images
40− post.extra.image ? (
41− <BackgroundImage
42− src={post.extra.image}
43− style={{ height: "720px" }}
44− className="w-full rounded-lg"
45− />
46− ) : null
47− }
48− <h2 className="mt-4">{post.title}</h2>
49− <div className="flex items-baseline">
80+ <div className="flex items-center measure pb-12">
50− {post.extra.author ? (
81+ <button
51− <div className="type-ui text-gray mt-4">
82+ onClick={() => {
52− {post.extra.author}
83+ setTab(0);
53− </div>
84+ setTypes(types);
54− ) : null}
85+ }}
55− {post.extra.author && post.extra.ship ? (
86+ className={`tab-button-sm mr-4 ${
56− <div className="mx-1 text-gray">•</div>
87+ tab === 0 ? "text-black" : "text-gray"
57− ) : null}
88+ }`}
58− {post.extra.ship ? (
89+ >
59− <div className="type-ui text-gray font-mono">
90+ All
60− {post.extra.ship}
91+ </button>
61− </div>
92+ <button
62− ) : null}
93+ onClick={() => {
94+ setTab(1);
95+ setTypes(["propsals"]);
96+ }}
97+ className={`tab-button-sm mr-4 ${
98+ tab === 1 ? "text-black" : "text-gray"
99+ }`}
100+ >
101+ Proposals
102+ </button>
103+ <button
104+ onClick={() => {
105+ setTab(2);
106+ setTypes(["bounties"]);
107+ }}
108+ className={`tab-button-sm mr-4 ${
109+ tab === 2 ? "text-black" : "text-gray"
110+ }`}
111+ >
112+ Bounties
113+ </button>
114+ <button
115+ onClick={() => {
116+ setTab(3);
117+ setTypes(["apprenticeships"]);
118+ }}
119+ className={`tab-button-sm mr-4 ${
120+ tab === 3 ? "text-black" : "text-gray"
121+ }`}
122+ >
123+ Apprenticeships
124+ </button>
125+ <button
126+ onClick={() => {
127+ setTab(4);
128+ setTypes(["gifts"]);
129+ }}
130+ className={`tab-button-sm ${
131+ tab === 4 ? "text-black" : "text-gray"
132+ }`}
133+ >
134+ Gifts
135+ </button>
63− </div>
136+ </div>
64
137
138+ <div className="flex flex-wrap mb-12">
139+ {categories.map((category) => {
140+ const isActive = activeTags.includes(category);
141+ const activeClasses = classnames({
142+ "bg-green text-white": isActive,
143+ "bg-wall text-black": !isActive,
144+ });
145+ return (
146+ <button
147+ onClick={() => setTags(toggleTag(activeTags, category))}
148+ className={`${activeClasses} badge-sm m-1`}
149+ >
150+ {category}
151+ </button>
152+ );
153+ })}
154+ </div>
155+
156+ {posts
157+ .reduce((acc, post) => {
158+ // Posts are returned if they match both the selected category and selected tags, or if the user has no category filters set.
159+ const itemDidPass =
160+ (post.taxonomies.grant_category.some((r) =>
161+ activeTags.includes(r)
162+ ) ||
163+ activeTags.length === 0) &&
164+ post.taxonomies.grant_type.some((r) => activeTypes.includes(r));
165+ // Return the item if the above check has passed
166+ if (itemDidPass === true) {
167+ return [...acc, post];
168+ }
169+ return acc;
170+ }, [])
171+ .map((post) => {
172+ return (
173+ <div
174+ key={post.slug}
175+ className="mb-4 cursor-pointer bg-wall rounded-lg"
176+ >
177+ <Link href={`/grants/${post.slug}`}>
65− <div className="type-ui text-gray mt-2">
178+ <div className="p-8">
66− {formatDate(new Date(post.date))}
179+ <h3 className="type-ui mb-4">{post.title}</h3>
180+ <p className="mb-4">{post.extra.description}</p>
181+ <div className="flex w-full items-center justify-between">
182+ <p className="text-gray">
183+ <strong>Reward: </strong>
184+ {post.extra.reward} star
185+ {post.extra.reward > 1 ? "s" : ""}
186+ </p>
187+ <div className="flex">
188+ {post.taxonomies.grant_category.map((category) => (
189+ <div className="bg-gray text-wall badge-sm m-1">
190+ {category}
191+ </div>
192+ ))}
193+ </div>
194+ </div>
67 </div>
195 </div>
68− </div>
196+ </Link>
69− </Link>
197+ </div>
70− </div>
71− );
198+ );
72− })}
199+ })}
73 </section>
200 </section>
74 </SingleColumn>
201 </SingleColumn>
75 <Footer />
202 <Footer />
@@ -80,15 +207,12 @@ export default function Grants({ posts }) {
80export async function getStaticProps() {
207export async function getStaticProps() {
81 const categories = getGrantsCategories();
208 const categories = getGrantsCategories();
82 const types = getGrantsTypes();
209 const types = getGrantsTypes();
83−
84− console.log(categories, types);
85−
86− // const posts = getAllPosts(
210+ const posts = getAllPosts(
87− // ["title", "slug", "date", "description", "extra"],
211+ ["title", "slug", "date", "description", "extra", "taxonomies"],
88− // "grants"
212+ "grants"
89− // );
213+ );
90
214
91 return {
215 return {
92− props: { posts: [] },
216+ props: { posts: posts, categories, types },
93 };
217 };
94}
218}
pages/index.js+10−6
@@ -60,9 +60,11 @@ export default function Home({ posts, events, openGrantsCount }) {
60 <div className="flex flex-col md:flex-row md:items-center measure pb-12">
60 <div className="flex flex-col md:flex-row md:items-center measure pb-12">
61 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
61 <h2 className="m-0 p-0 mr-4 md:mb-0 mb-4">Urbit Grants</h2>
62 <div>
62 <div>
63+ <Link href="/grants">
63− <button className="bg-green text-white badge-lg">
64+ <a className="bg-green text-white badge-lg">
64− {openGrantsCount} Open
65+ {openGrantsCount} Open
65− </button>
66+ </a>
67+ </Link>
66 </div>
68 </div>
67 </div>
69 </div>
68 <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">
@@ -88,9 +90,11 @@ export default function Home({ posts, events, openGrantsCount }) {
88 We regularly give away address space to reward community
90 We regularly give away address space to reward community
89 contributions, including education, promotion, and development.
91 contributions, including education, promotion, and development.
90 </p>
92 </p>
93+ <div className="table">
94+ <Link href="/grants">
91− <button className="button-lg bg-green text-white">
95+ <a className="button-lg bg-green text-white">View Grants</a>
92− View Grants
96+ </Link>
93− </button>
97+ </div>
94 </div>
98 </div>
95 </section>
99 </section>
96
100
styles/globals.css+13
@@ -49,6 +49,10 @@ h4,
49 @apply text-base md:text-lg lg:text-xl font-semibold;
49 @apply text-base md:text-lg lg:text-xl font-semibold;
50}
50}
51
51
52+.type-ui-small {
53+ @apply text-sm lg:text-base font-medium;
54+}
55+
52p,
56p,
53.type-p {
57.type-p {
54 @apply text-base md:text-lg lg:text-xl;
58 @apply text-base md:text-lg lg:text-xl;
@@ -79,6 +83,10 @@ button {
79 @apply flex px-4 lg:px-6 py-2 rounded-lg h-12 md:h-16 flex-center type-ui;
83 @apply flex px-4 lg:px-6 py-2 rounded-lg h-12 md:h-16 flex-center type-ui;
80}
84}
81
85
86+.badge-sm {
87+ @apply flex type-ui-small px-2 lg:px-3 items-center justify-center h-8 md:h-10 rounded-full;
88+}
89+
82.badge-lg {
90.badge-lg {
83 @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-12 md:h-16 rounded-full;
84}
92}
@@ -88,6 +96,11 @@ button {
88 overflow: visible;
96 overflow: visible;
89}
97}
90
98
99+.tab-button-sm {
100+ @apply type-ui h-12;
101+ overflow: visible;
102+}
103+
91.button-sm:hover {
104.button-sm:hover {
92 opacity: 0.66;
105 opacity: 0.66;
93}
106}
tailwind.config.js+18−17
@@ -1,29 +1,30 @@
1module.exports = {
1module.exports = {
2− purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
2+ purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"],
3 darkMode: false, // or 'media' or 'class'
3 darkMode: false, // or 'media' or 'class'
4 theme: {
4 theme: {
5 colors: {
5 colors: {
6− black: '#000',
6+ black: "#000",
7− white: '#FFF',
7+ white: "#FFF",
8− gray: 'rgba(0, 0, 0, 0.6)',
8+ gray: "rgba(0, 0, 0, 0.6)",
9− lightGray: 'rgba(0, 0, 0, 0.2)',
9+ lightGray: "rgba(0, 0, 0, 0.2)",
10− wall: 'rgba(244, 243, 241, 1)',
10+ wall: "rgba(244, 243, 241, 1)",
11− yellow: '#FCDB63',
11+ yellow: "#FCDB63",
12− midYellow: 'rgba(255, 199, 0, 0.6)',
12+ midYellow: "rgba(255, 199, 0, 0.6)",
13− lightYellow: 'rgba(255, 199, 0, 0.3)',
13+ lightYellow: "rgba(255, 199, 0, 0.3)",
14− washedYellow: 'rgba(255, 199, 0, 0.05)',
14+ washedYellow: "rgba(255, 199, 0, 0.05)",
15− green: 'rgba(0, 177, 113, 1)',
15+ green: "rgba(0, 177, 113, 1)",
16− midGreen: 'rgba(0, 159, 101, 0.6)',
16+ midGreen: "rgba(0, 159, 101, 0.6)",
17− lightGreen: 'rgba(0, 159, 101, 0.3)',
17+ lightGreen: "rgba(0, 159, 101, 0.3)",
18− washedGreen: 'rgba(0, 159, 101, 0.05)',
18+ washedGreen: "rgba(0, 159, 101, 0.05)",
19+ blue: "#219DFF",
19 },
20 },
20 fontFamily: {
21 fontFamily: {
21− sans: ['Inter', '-apple-system', 'sans-serif'],
22+ sans: ["Inter", "-apple-system", "sans-serif"],
22− mono: ['Source Code Pro', 'monospace'],
23+ mono: ["Source Code Pro", "monospace"],
23 },
24 },
24 },
25 },
25 variants: {
26 variants: {
26 extend: {},
27 extend: {},
27 },
28 },
28 plugins: [],
29 plugins: [],
29−}
30+};