on master help sign in
Merge pull request #1878 from urbit/mp/grant-bidirect
f0a1ac8matildepark
components/JoinGroup.js+2−2
@@ -58,9 +58,9 @@ export default function Contact({ emphasize, groupName, label, className }) {
58
58
59 return (
59 return (
60 <div className={className}>
60 <div className={className}>
61− <h4 className="mt-4 text-wall-500 flex items-baseline flex-wrap">
61+ <h4 className="text-wall-500 flex items-baseline flex-wrap">
62 {label ? label + " " : ""}
62 {label ? label + " " : ""}
63− <div className="md:ml-2 rounded-lg flex items-stretch">
63+ <div className="rounded-lg flex items-stretch">
64 <code
64 <code
65 className={`rounded-l-lg px-2 md:px-4 py-2 ${washedBg} flex items-center h-full ${linkText}`}
65 className={`rounded-l-lg px-2 md:px-4 py-2 ${washedBg} flex items-center h-full ${linkText}`}
66 >
66 >
components/ecosystem/BasicPage.js+19−18
@@ -12,6 +12,7 @@ import {
12} from "@urbit/foundation-design-system";
12} from "@urbit/foundation-design-system";
13import Header from "../Header";
13import Header from "../Header";
14import Footer from "../Footer";
14import Footer from "../Footer";
15+import MetadataBlock from "./MetadataBlock";
15
16
16export default function BasicPage({
17export default function BasicPage({
17 section,
18 section,
@@ -28,7 +29,7 @@ export default function BasicPage({
28 metaPost.title = `${post.title} - ${section}`;
29 metaPost.title = `${post.title} - ${section}`;
29 metaPost.description =
30 metaPost.description =
30 section === "Podcast" ? `${post.podcast} - ${post.date}` : post.description;
31 section === "Podcast" ? `${post.podcast} - ${post.date}` : post.description;
31− section === "Article" ? `${post.publication} - ${post.date}` : post.description;
32+ section === "Article" ? `${post.publication} - ${post.date}` : post.description;
32 return (
33 return (
33 <Container>
34 <Container>
34 <Head>
35 <Head>
@@ -44,7 +45,7 @@ export default function BasicPage({
44 <div className="flex items-center space-x-4">
45 <div className="flex items-center space-x-4">
45 <img src={post.image} className="w-36" />
46 <img src={post.image} className="w-36" />
46 <div className="flex flex-col pl-2">
47 <div className="flex flex-col pl-2">
47− <h1 className={classnames({ "text-3xl": section === "Podcast" },{ "text-3xl": section === "Article" })}>
48+ <h1 className={classnames({ "text-3xl": section === "Podcast" }, { "text-3xl": section === "Article" })}>
48 {post.title}
49 {post.title}
49 </h1>
50 </h1>
50 <p>{section}</p>
51 <p>{section}</p>
@@ -54,28 +55,28 @@ export default function BasicPage({
54 <div className="flex justify-between">
55 <div className="flex justify-between">
55 <div className="flex space-x-12">
56 <div className="flex space-x-12">
56 {post?.podcast && (
57 {post?.podcast && (
57− <div className="flex flex-col">
58+ <MetadataBlock
58− <p className="font-bold text-wall-400">Podcast</p>
59+ title="Podcast"
59− <p>{post.podcast}</p>
60+ content={post.podcast}
60− </div>
61+ />
61 )}
62 )}
62 {post?.publication && (
63 {post?.publication && (
63− <div className="flex flex-col">
64+ <MetadataBlock
64− <p className="font-bold text-wall-400">Publication</p>
65+ title="Publication"
65− <p>{post.publication}</p>
66+ content={post.publication}
66− </div>
67+ />
67 )}
68 )}
68 {post?.author && (
69 {post?.author && (
69− <div className="flex flex-col">
70+ <MetadataBlock
70− <p className="font-bold text-wall-400">Author</p>
71+ title="Author"
71− <p>{post.author}</p>
72+ content={post.author}
72− </div>
73+ />
73 )}
74 )}
74 {post?.date && (
75 {post?.date && (
75− <div className="flex flex-col">
76+ <MetadataBlock
76− <p className="font-bold text-wall-400">Date</p>
77+ title="Date"
77− <p className="shrink-0">{post.date}</p>
78+ content={post.date}
78− </div>
79+ />
79 )}
80 )}
80
81
81 {post?.URL && section !== "Podcast" && section !== "Article" && (
82 {post?.URL && section !== "Podcast" && section !== "Article" && (
components/ecosystem/MetadataBlock.jsnew file+7
@@ -0,0 +1,7 @@
1+export default function MetadataBlock({ title, content }) {
2+ return <div className="flex flex-col py-2 basis-1/2 md:basis-auto">
3+ <p className="font-bold text-wall-400">{title}</p>
4+ <p>{content}</p>
5+ </div>
6+
7+}
components/gateway/Description.js−1
@@ -4,7 +4,6 @@ const Description = ({ description, fallback, markdown }) => {
4 return (
4 return (
5 (description !== fallback || markdown) && (
5 (description !== fallback || markdown) && (
6 <div className="flex flex-col space-y-4">
6 <div className="flex flex-col space-y-4">
7− <p className="font-bold text-wall-400">Description</p>
8 {markdown ? (
7 {markdown ? (
9 <div className="markdown">
8 <div className="markdown">
10 <Markdown.render content={JSON.parse(markdown)} />
9 <Markdown.render content={JSON.parse(markdown)} />
components/gateway/Image.js+4−4
@@ -9,7 +9,7 @@ const GatewayImage = ({
9}) => {
9}) => {
10 if (error) {
10 if (error) {
11 return (
11 return (
12− <div className="overflow-hidden rounded-xl">
12+ <div className="overflow-hidden rounded-xl shrink-0">
13 <svg
13 <svg
14 width={size}
14 width={size}
15 height={size}
15 height={size}
@@ -29,7 +29,7 @@ const GatewayImage = ({
29
29
30 if (patp) {
30 if (patp) {
31 return (
31 return (
32− <div className="rounded-xl overflow-hidden">
32+ <div className="rounded-xl overflow-hidden shrink-0">
33 <Sigil patp={patp} size={size} color={color} />
33 <Sigil patp={patp} size={size} color={color} />
34 </div>
34 </div>
35 );
35 );
@@ -39,14 +39,14 @@ const GatewayImage = ({
39 return (
39 return (
40 <div
40 <div
41 style={{ height: size, width: size, backgroundColor: image }}
41 style={{ height: size, width: size, backgroundColor: image }}
42− className="rounded-xl"
42+ className="rounded-xl shrink-0"
43 />
43 />
44 );
44 );
45 }
45 }
46
46
47 return image ? (
47 return image ? (
48 <div
48 <div
49− className="rounded-xl overflow-hidden"
49+ className="rounded-xl overflow-hidden shrink-0"
50 style={{ backgroundColor: color }}
50 style={{ backgroundColor: color }}
51 >
51 >
52 <img src={image} style={{ height: size, width: size }} />
52 <img src={image} style={{ height: size, width: size }} />
components/gateway/ResourceCard.js+14−3
@@ -1,11 +1,22 @@
1import Link from "next/link";
1import Link from "next/link";
2import GatewayImage from "./Image";
2import GatewayImage from "./Image";
3+import classNames from "classnames";
4+import { capitalize } from "@urbit/foundation-design-system";
3
5
4−const ResourceCard = ({ type, shortcode, title, color, image }) => (
6+const ResourceCard = ({ type, shortcode, title, description = "", color, image, full }) => (
5 <Link href={`/${type}s/${shortcode}`}>
7 <Link href={`/${type}s/${shortcode}`}>
6− <div className="cursor-pointer rounded-xl bg-wall-100 p-4 flex items-center space-x-4 mr-2 my-1">
8+ <div className={classNames("cursor-pointer rounded-xl bg-wall-100 p-4 flex items-center space-x-4 mr-2 my-1", {
9+ "basis-full": full
10+ })}>
7 <GatewayImage color={color} image={image || color} size={50} />
11 <GatewayImage color={color} image={image || color} size={50} />
12+ {full
13+ ? <><div className="flex flex-col w-full">
14+ <a className="font-semibold">{title}</a>
15+ <p className="hidden lg:block">{description}</p>
16+ </div>
17+ <a className="button-lg bg-green-400 text-white shrink-0">View {capitalize(type)}</a>
18+ </>
8− <a className="font-semibold">{title}</a>
19+ : <a className="font-semibold">{title}</a>}
9 </div>
20 </div>
10 </Link>
21 </Link>
11);
22);
content/grants/2022/faux.md+2−1
@@ -16,7 +16,8 @@ champion = ["~littel-wolfur"]
16grant_id = "P0145"
16grant_id = "P0145"
17completed = true
17completed = true
18canceled = false
18canceled = false
19−link = ""
19+link = "~tomdys/the-faux-shore"
20+deliverable = "~midsum-salrux/faux"
20
21
21+++
22+++
22
23
lib/cache.js+5−1
@@ -15,10 +15,14 @@ const options = {
15const index = [];
15const index = [];
16
16
17function buildSearchIndex(dir) {
17function buildSearchIndex(dir) {
18+ // Assume years belong to Grants for now.
18 const dirName = dir.split("/").slice(-1).join("");
19 const dirName = dir.split("/").slice(-1).join("");
20+ const parentName = String(Number(dirName)).length === 4
21+ ? "Grants"
22+ : dirName.charAt(0).toUpperCase() + dirName.slice(1);
19 let metadata = {
23 let metadata = {
20 data: {
24 data: {
21− title: dirName.charAt(0).toUpperCase() + dirName.slice(1),
25+ title: parentName,
22 },
26 },
23 };
27 };
24
28
lib/lib.js+32
@@ -126,6 +126,38 @@ export function getGrantsTypes() {
126 return uniqueTypes;
126 return uniqueTypes;
127}
127}
128
128
129+// Grab which directory the grant post is in
130+
131+export function getGrantYear(slug) {
132+ const basePath = join(process.cwd(), 'content/grants')
133+ const years = fs.readdirSync(basePath, { 'withFileTypes': true }).filter((f) => f.isDirectory());
134+ const dir = years.find((year) => {
135+ const yearDir = fs.readdirSync(join(basePath, year.name), { 'withFileTypes': true });
136+ return yearDir.some((file) => file.name === `${slug}.md`)
137+ });
138+ return dir;
139+}
140+
141+export function getGrantsByShortcode(shortcode) {
142+ const basePath = join(process.cwd(), 'content/grants')
143+ const years = fs.readdirSync(basePath, { 'withFileTypes': true }).filter((f) => f.isDirectory());
144+ let found = [];
145+ // Get matching years by any matching grant.
146+ years.forEach((year) => {
147+ const yearDir = fs.readdirSync(join(basePath, year.name), { 'withFileTypes': true });
148+ return yearDir.forEach((file) => {
149+ const { data } = matter(fs.readFileSync(join(basePath, year.name, file.name)), options);
150+ if (data?.extra?.deliverable === shortcode.join("/")) {
151+ found.push({
152+ ...data, ...{ slug: file.name.replace('.md', '') }
153+ }
154+ )
155+ }
156+ })
157+ });
158+ return found
159+}
160+
129// Groups arrays into an array of arrays with 2 members
161// Groups arrays into an array of arrays with 2 members
130export const pair = (arr) =>
162export const pair = (arr) =>
131 arr.reduce(function (rows, key, index) {
163 arr.reduce(function (rows, key, index) {
pages/applications/[[...application]].js+14−5
@@ -19,8 +19,10 @@ import Gateway404 from "../../components/gateway/Gateway404";
19import MetadataBlock from "../../components/gateway/MetadataBlock";
19import MetadataBlock from "../../components/gateway/MetadataBlock";
20import MetadataLink from "../../components/gateway/MetadataLink";
20import MetadataLink from "../../components/gateway/MetadataLink";
21import Description from "../../components/gateway/Description";
21import Description from "../../components/gateway/Description";
22+import { getGrantsByShortcode } from "../../lib/lib";
23+import GrantPreview from "../../components/GrantPreview";
22
24
23−const ApplicationPage = ({ data, markdown, organisation, search, params }) => {
25+const ApplicationPage = ({ data, markdown, organisation, matchedGrants, search, params }) => {
24 const { application } = params;
26 const { application } = params;
25 if (!ob.isValidPatp(application[0])) {
27 if (!ob.isValidPatp(application[0])) {
26 return <Gateway404 type="application" />;
28 return <Gateway404 type="application" />;
@@ -83,7 +85,7 @@ const ApplicationPage = ({ data, markdown, organisation, search, params }) => {
83 item="Application"
85 item="Application"
84 />
86 />
85
87
86− <div className="flex flex-wrap md:flex-nowrap justify-between">
88+ <div className="flex flex-wrap justify-between">
87 <MetadataBlock
89 <MetadataBlock
88 title="License"
90 title="License"
89 content={data.license ? data.license : "Unknown"}
91 content={data.license ? data.license : "Unknown"}
@@ -103,14 +105,18 @@ const ApplicationPage = ({ data, markdown, organisation, search, params }) => {
103 content={data.shortcode ? data.shortcode : data.title}
105 content={data.shortcode ? data.shortcode : data.title}
104 />
106 />
105 </div>
107 </div>
106−
107 <Description
108 <Description
108 description={data.description}
109 description={data.description}
109 fallback="An application on Urbit."
110 fallback="An application on Urbit."
110 markdown={markdown}
111 markdown={markdown}
111 />
112 />
112−
113− <hr className="text-white" />
113+ <hr className="text-wall-200" />
114+ {matchedGrants && <div className="basis-full flex flex-col space-y-2">
115+ <p className="font-semibold text-wall-400">Related Grants</p>
116+ {matchedGrants.map((grant) => {
117+ return <GrantPreview grant={grant} />
118+ })}
119+ </div>}
114
120
115 <div class="bg-wall-100 py-4 px-6 border-2 border-wall-200 rounded-xl">
121 <div class="bg-wall-100 py-4 px-6 border-2 border-wall-200 rounded-xl">
116 <p class="text-sm text-wall-400">
122 <p class="text-sm text-wall-400">
@@ -175,6 +181,8 @@ export const getServerSideProps = async ({ params, res }) => {
175 ? JSON.stringify(Markdown.parse({ post: { content } }))
181 ? JSON.stringify(Markdown.parse({ post: { content } }))
176 : null;
182 : null;
177
183
184+ const matchedGrants = getGrantsByShortcode(params.application) || null;
185+
178 if (!data.title) {
186 if (!data.title) {
179 data = {
187 data = {
180 title: params.application?.join("/"),
188 title: params.application?.join("/"),
@@ -187,6 +195,7 @@ export const getServerSideProps = async ({ params, res }) => {
187 data,
195 data,
188 markdown,
196 markdown,
189 organisation,
197 organisation,
198+ matchedGrants,
190 params,
199 params,
191 },
200 },
192 };
201 };
pages/grants/[slug].js+107−42
@@ -11,17 +11,21 @@ import {
11 Section,
11 Section,
12 IntraNav,
12 IntraNav,
13 getPostBySlug,
13 getPostBySlug,
14− getAllPosts,
14+ getPage,
15 formatDate,
15 formatDate,
16} from "@urbit/foundation-design-system";
16} from "@urbit/foundation-design-system";
17import Header from "../../components/Header";
17import Header from "../../components/Header";
18import Footer from "../../components/Footer";
18import Footer from "../../components/Footer";
19+import MetadataBlock from "../../components/ecosystem/MetadataBlock";
20+import ResourceCard from "../../components/gateway/ResourceCard";
21+import JoinGroup from "../../components/JoinGroup";
19import ob from "urbit-ob";
22import ob from "urbit-ob";
20import { DateTime } from "luxon";
23import { DateTime } from "luxon";
21import fs from 'fs';
24import fs from 'fs';
22−import { join } from 'path';
25+import path, { join } from 'path';
26+import { getGrantYear } from "../../lib/lib";
23
27
24−export default function Grant({ post, markdown, search }) {
28+export default function Grant({ post, markdown, match, search }) {
25 const router = useRouter();
29 const router = useRouter();
26 if (!router.isFallback && !post?.slug) {
30 if (!router.isFallback && !post?.slug) {
27 return <ErrorPage />;
31 return <ErrorPage />;
@@ -65,6 +69,45 @@ export default function Grant({ post, markdown, search }) {
65 );
69 );
66 });
70 });
67
71
72+ let status = "";
73+ if (isOpen) {
74+ status = "Open"
75+ } else if (post?.extra?.completed) {
76+ status = "Completed"
77+ } else if (post?.extra?.canceled) {
78+ status = "Canceled"
79+ } else {
80+ status = "In Progress"
81+ }
82+ const statusBadge = <div className={
83+ classnames("badge-sm mr-1 my-1", {
84+ "bg-red": status === "Canceled",
85+ "bg-green-400 text-white": status === "Completed" || status === "Open",
86+ "bg-wall-300": status === "In Progress",
87+ })}>
88+ {status}
89+ </div>;
90+
91+ let deliverable = null;
92+ if (post.extra.deliverable) {
93+ if (match) {
94+ deliverable = <ResourceCard
95+ title={match.data.title}
96+ image={match.data.image}
97+ color={match.data.bgColor || "#000000"}
98+ description={match.data?.description || ""}
99+ type="application"
100+ shortcode={post.extra.deliverable}
101+ full
102+ />
103+ }
104+ else if (post.extra.deliverable.startsWith("~")) {
105+ deliverable = <JoinGroup emphasize groupName={post.extra.deliverable} />
106+ } else {
107+ deliverable = <a className="font-medium text-green-400" href={post.extra.deliverable}>{post.extra.deliverable}</a>
108+ }
109+ }
110+
68 const metaPost = Object.assign({}, post);
111 const metaPost = Object.assign({}, post);
69 metaPost.title = `${post.title} - Grant`;
112 metaPost.title = `${post.title} - Grant`;
70
113
@@ -79,38 +122,8 @@ export default function Grant({ post, markdown, search }) {
79 <Header search={search} />
122 <Header search={search} />
80 <Section narrow short>
123 <Section narrow short>
81 <h1>{post.title}</h1>
124 <h1>{post.title}</h1>
82− {post.extra.assignee ? (
83− <div className="type-ui text-wall-500 mt-4">
84− Grantee(s): {assigneeLink}
85− </div>
86− ) : null}
87− {post.extra.mentor ? (
88− <div className="type-ui text-wall-500 mt-4">
89− Mentor: {mentorLink}
90− </div>
91− ) : null}
92− {post.extra.champion ? (
93− <div className="type-ui text-wall-500 mt-4">
94− Champion: {mentorLink}
95− </div>
96− ) : null}
97− {post.extra.ship ? (
98− <Link href={`/ids/${post.extra.ship}`} passHref>
99− <a className="type-sub-bold text-wall-500 font-mono">
100− {post.extra.ship}
101− </a>
102− </Link>
103− ) : null}
104− <div className="type-ui text-wall-500 mt-4 md:mt-8 lg:mt-10 flex items-center space-x-1">
105− {formatDate(DateTime.fromISO(post.date))}
106− {post?.extra?.grant_id ? (
107− <>
108− <p className="ml-1 text-wall-500 type-ui">• ID:</p>
109− <p className="font-mono type-ui"> {post.extra.grant_id}</p>
110− </>
111− ) : null}
112− </div>
113− <div className="flex items-center flex-wrap mt-4 md:mt-8 lg:mt-10">
125+ <div className="flex items-center flex-wrap my-4">
126+ {statusBadge}
114 {post.taxonomies.grant_type.map((category) => {
127 {post.taxonomies.grant_type.map((category) => {
115 const className = classnames({
128 const className = classnames({
116 "bg-blue-400 text-white": category === "Proposal",
129 "bg-blue-400 text-white": category === "Proposal",
@@ -129,6 +142,46 @@ export default function Grant({ post, markdown, search }) {
129 </div>
142 </div>
130 ))}
143 ))}
131 </div>
144 </div>
145+ <div className="flex flex-wrap md:flex-nowrap justify-between">
146+ {post?.extra?.grant_id ? (
147+ <>
148+ <MetadataBlock
149+ title="ID"
150+ content={post.extra.grant_id}
151+ />
152+ </>
153+ ) : null}
154+ <MetadataBlock
155+ title="Date"
156+ content={formatDate(DateTime.fromISO(post.date))}
157+ />
158+ {post.extra.mentor ? (
159+ <MetadataBlock
160+ title="Mentor"
161+ content={mentorLink}
162+ />
163+ ) : null}
164+ {post.extra.champion ? (
165+ <MetadataBlock
166+ title="Champion"
167+ content={mentorLink}
168+ />
169+ ) : null}
170+ {post.extra.assignee ? (
171+ <MetadataBlock
172+ title="Grantee(s)"
173+ content={assigneeLink}
174+ />
175+ ) : null}
176+ </div>
177+ {post?.extra?.link?.startsWith("~") && <div className="flex flex-col my-4 space-y-2">
178+ <p className="text-wall-400 font-semibold">Group</p>
179+ <JoinGroup emphasize groupName={post.extra.link} />
180+ </div>}
181+ {deliverable && <div className="flex flex-col my-4 space-y-2">
182+ <p className="text-wall-400 font-semibold">Deliverable</p>
183+ {deliverable}
184+ </div>}
132 </Section>
185 </Section>
133 <Section narrow className="markdown">
186 <Section narrow className="markdown">
134 <Markdown.render content={JSON.parse(markdown)} />
187 <Markdown.render content={JSON.parse(markdown)} />
@@ -149,12 +202,8 @@ export default function Grant({ post, markdown, search }) {
149}
202}
150
203
151export async function getStaticProps({ params }) {
204export async function getStaticProps({ params }) {
152− const basePath = join(process.cwd(), 'content/grants')
205+ // Gets post in any year folder sharing the slug name in the param
153− const years = fs.readdirSync(basePath, { 'withFileTypes': true }).filter((f) => f.isDirectory());
206+ const dir = getGrantYear(params.slug);
154− const dir = years.find((year) => {
155− const yearDir = fs.readdirSync(join(basePath, year.name), { 'withFileTypes': true });
156− return yearDir.some((file) => file.name === `${params.slug}.md`)
157− });
158 const post = getPostBySlug(
207 const post = getPostBySlug(
159 params.slug,
208 params.slug,
160 ["title", "slug", "date", "description", "content", "extra", "taxonomies"],
209 ["title", "slug", "date", "description", "content", "extra", "taxonomies"],
@@ -162,12 +211,28 @@ export async function getStaticProps({ params }) {
162 )
211 )
163
212
164 const markdown = JSON.stringify(Markdown.parse({ post }));
213 const markdown = JSON.stringify(Markdown.parse({ post }));
214+
215+ // Matches deliverable to page
216+ let match = null;
217+ if (post?.extra?.deliverable?.startsWith("~")) {
218+ const ship = post.extra.deliverable.split("/")[0];
219+ const application = post.extra.deliverable.split("/")[1];
220+ const fileExists = fs.existsSync(path.join(process.cwd(), "content/applications", ship, `${application}.md`));
221+ match = fileExists ? getPage(
222+ join(
223+ process.cwd(),
224+ "content/applications",
225+ post.extra.deliverable
226+ )
227+ ) : null
228+ }
165 return {
229 return {
166− props: { post, markdown },
230+ props: { post, markdown, match },
167 };
231 };
168}
232}
169
233
170export async function getStaticPaths() {
234export async function getStaticPaths() {
235+ // Flatten all year subfolders into a flat array of slugs
171 const basePath = join(process.cwd(), 'content/grants')
236 const basePath = join(process.cwd(), 'content/grants')
172 const years = fs.readdirSync(basePath, { 'withFileTypes': true });
237 const years = fs.readdirSync(basePath, { 'withFileTypes': true });
173 const posts = years.filter((year) => year.isDirectory()).map((year) => {
238 const posts = years.filter((year) => year.isDirectory()).map((year) => {
pages/organizations/[slug].js+1−1
@@ -52,7 +52,7 @@ export default function Organization({
52 }}
52 }}
53 >
53 >
54 {app.image && (
54 {app.image && (
55− <img src={app.image} className="h-full w-full" />
55+ <img src={app.image} className="h-full w-full" style={{ aspectRatio: "1" }} />
56 )}
56 )}
57 </div>
57 </div>
58 <p className="text-center font-bold font-xl">{app.title}</p>
58 <p className="text-center font-bold font-xl">{app.title}</p>