on master help sign in
Merge pull request #1919 from urbit/mp/eco-simple
d86460cGordon Cieplak
components/ecosystem/Badge.jsnew file+20
@@ -0,0 +1,20 @@
1+import classnames from "classnames";
2+export default function EcosystemBadge({ type }) {
3+ return (
4+ <div
5+ className={classnames(
6+ "uppercase font-bold text-white text-xs w-fit rounded-lg mb-2 px-2 py-1",
7+ {
8+ "bg-[#B37ED7]": type === "Organization",
9+ "bg-[#83b2d4]": type === "Application",
10+ "bg-wall-600": type === "Podcast",
11+ "bg-[#8dd5b1]": type === "Marketplace",
12+ "bg-[#B5966D]": type === "Article",
13+ "bg-[#219DFF]": type === "Group",
14+ }
15+ )}
16+ >
17+ {type}
18+ </div>
19+ );
20+}
components/ecosystem/EcosystemSidebar.jsnew file+78
@@ -0,0 +1,78 @@
1+import { useRouter } from "next/router";
2+import Link from "next/link";
3+import classnames from "classnames";
4+function ActiveLink({ children, href, className, currentPath }) {
5+ // special casing this behaviour which overloads one nav
6+ const archive = Boolean(
7+ currentPath === "/ecosystem?type=archive" && href === "/ecosystem"
8+ );
9+ const activeClassName = classnames({
10+ "text-wall-800": currentPath === href || archive,
11+ "text-wall-400": currentPath !== href && !archive,
12+ });
13+
14+ return (
15+ <Link href={href} passHref>
16+ <a className={`${className} ${activeClassName}`}>{children}</a>
17+ </Link>
18+ );
19+}
20+
21+export default function EcosystemSidebar() {
22+ const router = useRouter();
23+ let currentPath = router.asPath;
24+ return (
25+ <div className="flex flex-col space-y-4 pr-24">
26+ <ActiveLink
27+ currentPath={currentPath}
28+ className="type-ui"
29+ href="/ecosystem"
30+ >
31+ Spotlight
32+ </ActiveLink>
33+ <ActiveLink
34+ currentPath={currentPath}
35+ className="type-ui"
36+ href="/ecosystem?type=organizations"
37+ >
38+ Organizations
39+ </ActiveLink>
40+ <ActiveLink
41+ currentPath={currentPath}
42+ className="type-ui"
43+ href="/ecosystem?type=applications"
44+ >
45+ Applications
46+ </ActiveLink>
47+ <ActiveLink
48+ currentPath={currentPath}
49+ className="type-ui"
50+ href="/ecosystem?type=groups"
51+ >
52+ Groups
53+ </ActiveLink>
54+ <ActiveLink
55+ currentPath={currentPath}
56+ className="type-ui"
57+ href="/ecosystem?type=marketplaces"
58+ >
59+ Marketplaces
60+ </ActiveLink>
61+ <ActiveLink
62+ currentPath={currentPath}
63+ className="mr-5 type-ui"
64+ href="/ecosystem?type=podcasts"
65+ >
66+ Podcasts
67+ </ActiveLink>
68+
69+ <ActiveLink
70+ currentPath={currentPath}
71+ className="mr-5 type-ui"
72+ href="/ecosystem?type=articles"
73+ >
74+ Articles
75+ </ActiveLink>
76+ </div>
77+ );
78+}
components/ecosystem/IndexCard.js+2−22
@@ -1,5 +1,5 @@
1import Link from "next/link";
1import Link from "next/link";
2−import classnames from "classnames";
2+import EcosystemBadge from "./Badge";
3export default function IndexCard({ slug, feat }) {
3export default function IndexCard({ slug, feat }) {
4 return (
4 return (
5 <Link href={slug}>
5 <Link href={slug}>
@@ -24,7 +24,7 @@ export default function IndexCard({ slug, feat }) {
24 )}
24 )}
25 <div className="pt-4">
25 <div className="pt-4">
26 <p className="mb-2 font-semibold leading-5">{feat.title}</p>
26 <p className="mb-2 font-semibold leading-5">{feat.title}</p>
27− {badge(feat.type)}
27+ <EcosystemBadge type={feat.type} />
28 <p className="text-sm leading-5">{feat?.description}</p>
28 <p className="text-sm leading-5">{feat?.description}</p>
29 </div>
29 </div>
30 </div>
30 </div>
@@ -32,23 +32,3 @@ export default function IndexCard({ slug, feat }) {
32 </Link>
32 </Link>
33 );
33 );
34}
34}
35−
36−const badge = (type) => {
37− return (
38− <div
39− className={classnames(
40− "uppercase font-bold text-white text-xs w-fit rounded-lg mb-5 px-2 py-1",
41− {
42− "bg-[#B37ED7]": type === "Organization",
43− "bg-[#83b2d4]": type === "Application",
44− "bg-[#219DFF]": type === "Group",
45− "bg-wall-600": type === "Podcast",
46− "bg-[#8dd5b1]": type === "Marketplace",
47− "bg-[#B5966D]": type === "Article",
48− }
49− )}
50− >
51− {type}
52− </div>
53− );
54−};
components/ecosystem/Post.js+4−21
@@ -1,5 +1,6 @@
1import { Markdown } from "@urbit/foundation-design-system";
1import { Markdown } from "@urbit/foundation-design-system";
2−import classnames from "classnames";
2+import EcosystemBadge from "./Badge";
3+
3import Link from "next/link";
4import Link from "next/link";
4export default function Post({ post }) {
5export default function Post({ post }) {
5 return ["featured-1", "featured-2", "featured-3"].map((feat) => {
6 return ["featured-1", "featured-2", "featured-3"].map((feat) => {
@@ -34,7 +35,7 @@ export default function Post({ post }) {
34 <h4 className="text-xl pb-2 leading-6 font-bold">
35 <h4 className="text-xl pb-2 leading-6 font-bold">
35 {post?.[feat].title}
36 {post?.[feat].title}
36 </h4>
37 </h4>
37− {badge(post[feat].type)}
38+ <EcosystemBadge type={post[feat].type} />
38 </div>
39 </div>
39 </div>
40 </div>
40 <div className="markdown mt-8 mb-8">
41 <div className="markdown mt-8 mb-8">
@@ -94,22 +95,4 @@ export default function Post({ post }) {
94 });
95 });
95}
96}
96
97
97−const badge = (type) => {
98+
98− return (
99− <div
100− className={classnames(
101− "uppercase font-bold text-white text-xs w-fit rounded-lg mb-2 px-2 py-1",
102− {
103− "bg-[#B37ED7]": type === "Organization",
104− "bg-[#83b2d4]": type === "Application",
105− "bg-wall-600": type === "Podcast",
106− "bg-[#8dd5b1]": type === "Marketplace",
107− "bg-[#B5966D]": type === "Article",
108− "bg-[#219DFF]": type === "Group",
109− }
110− )}
111− >
112− {type}
113− </div>
114− );
115−};
components/ecosystem/SpotlightArchive.jsnew file+66
@@ -0,0 +1,66 @@
1+import { Container, IntraNav, SingleColumn, Section } from "@urbit/foundation-design-system";
2+import Header from "../Header";
3+import Footer from "../Footer";
4+import Meta from "../Meta";
5+import Head from "next/head";
6+import Link from "next/link";
7+import EcosystemSidebar from "./EcosystemSidebar";
8+import Sidebar from "../Sidebar";
9+
10+export default function SpotlightArchive({ posts, search }) {
11+ const post = {
12+ title: "Spotlight Archive",
13+ description: "The archive of ecosystem spotlights.",
14+ };
15+ return (
16+ <Container>
17+ <Head>
18+ <title>Archive • Spotlight • Urbit</title>
19+ {Meta(post)}
20+ </Head>
21+ <IntraNav ourSite="https://urbit.org" search={search} />
22+ <SingleColumn>
23+ <Header search={search} />
24+ <Section>
25+ <div className="measure">
26+ <h1>Ecosystem</h1>
27+ </div>
28+ </Section>
29+ <Section className="flex">
30+ <Sidebar>
31+ <EcosystemSidebar />
32+ </Sidebar>
33+ <div>
34+ <h3 className="mb-12">Spotlight Archive</h3>
35+ {posts.map((post) => {
36+ return (
37+ <div key={post.slug} className="mb-24 cursor-pointer">
38+ <Link href={`/ecosystem/spotlight/${post.slug}`}>
39+ <div>
40+ <h3 className="text-green-400">{post.title}</h3>
41+ <div className="type-ui mt-2">
42+ {["featured-1", "featured-2", "featured-3"].map(
43+ (feat, i) => {
44+ if (post?.[feat]) {
45+ return (
46+ <>
47+ {i ? ", " : ""}
48+ <span>{post[feat].title}</span>
49+ </>
50+ );
51+ }
52+ }
53+ )}
54+ </div>
55+ </div>
56+ </Link>
57+ </div>
58+ );
59+ })}
60+ </div>
61+ </Section>
62+ </SingleColumn>
63+ <Footer />
64+ </Container>
65+ );
66+}
lib/lib.js+84−1
@@ -3,7 +3,7 @@ import { join, parse } from "path";
3import matter from "gray-matter";
3import matter from "gray-matter";
4import toml from "@iarna/toml";
4import toml from "@iarna/toml";
5import { DateTime } from "luxon";
5import { DateTime } from "luxon";
6−import { getPostBySlug, getPostSlugs } from "@urbit/foundation-design-system";
6+import { Markdown, getPostBySlug, getPostSlugs } from "@urbit/foundation-design-system";
7
7
8// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
8// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
9const options = {
9const options = {
@@ -166,3 +166,86 @@ export const pair = (arr) =>
166 rows
166 rows
167 );
167 );
168 }, []);
168 }, []);
169+
170+// Match ecosystem report to posts to inject metadata
171+export const matchEcosystemPost = (ecosystem) => {
172+ const articles = getAllPosts(
173+ ["title", "image", "date", "podcast", "slug"],
174+ "articles",
175+ "date"
176+ );
177+
178+ const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
179+ const podcasts = getAllPosts(
180+ ["title", "image", "date", "podcast", "slug"],
181+ "podcasts",
182+ "date"
183+ );
184+ const organizations = getAllPosts(
185+ ["title", "image", "slug"],
186+ "organizations"
187+ );
188+ const applications = fs
189+ .readdirSync(join(process.cwd(), "content/applications"), {
190+ withFileTypes: true,
191+ })
192+ .filter((f) => f.isDirectory())
193+ .map((dir) =>
194+ getAllPosts(
195+ ["title", "bgColor", "image", "slug", "description"],
196+ `applications/${dir.name}`
197+ )
198+ .map((e) => ({ ...e, ship: dir.name }))
199+ .flat()
200+ )
201+ .flat()
202+ .sort((a, b) => {
203+ const nameA = a.title.toLowerCase();
204+ const nameB = b.title.toLowerCase();
205+ return nameA < nameB ? -1 : 1;
206+ });
207+
208+ const groups = fs
209+ .readdirSync(join(process.cwd(), "content/groups"), {
210+ withFileTypes: true,
211+ })
212+ .filter((f) => f.isDirectory())
213+ .map((dir) =>
214+ getAllPosts(
215+ ["title", "tile", "slug", "description"],
216+ `groups/${dir.name}`
217+ )
218+ .map((e) => ({ ...e, ship: dir.name }))
219+ .flat()
220+ )
221+ .flat()
222+ .sort((a, b) => {
223+ const nameA = a.title.toLowerCase();
224+ const nameB = b.title.toLowerCase();
225+ return nameA < nameB ? -1 : 1;
226+ });
227+
228+ ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
229+ if (ecosystem?.[feat]) {
230+ const matchedPost = [
231+ ...articles.map((e) => ({ ...e, type: "Article" })),
232+ ...applications.map((e) => ({ ...e, type: "Application" })),
233+ ...organizations.map((e) => ({ ...e, type: "Organization" })),
234+ ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
235+ ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
236+ ...groups.map((e) => ({ ...e, type: "Group" }))
237+ ].filter((e) => e.title === ecosystem[feat].title)?.[0];
238+ ecosystem[feat].image =
239+ ecosystem[feat].image || matchedPost?.image || null;
240+ ecosystem[feat].type = matchedPost?.type || "Podcast";
241+ ecosystem[feat].matchedPost = matchedPost || null;
242+ ecosystem[feat].content = JSON.stringify(
243+ Markdown.parse({
244+ post: { content: ecosystem[feat].content },
245+ })
246+ );
247+ }
248+ });
249+
250+ return ecosystem;
251+}
pages/ecosystem/index.js+138−261
@@ -6,7 +6,6 @@ import {
6 SingleColumn,
6 SingleColumn,
7 Section,
7 Section,
8 IntraNav,
8 IntraNav,
9− Markdown,
10 getAllPosts,
9 getAllPosts,
11 generateDisplayDate,
10 generateDisplayDate,
12 formatDate,
11 formatDate,
@@ -14,12 +13,15 @@ import {
14import Header from "../../components/Header";
13import Header from "../../components/Header";
15import Footer from "../../components/Footer";
14import Footer from "../../components/Footer";
16import Sidebar from "../../components/Sidebar";
15import Sidebar from "../../components/Sidebar";
16+import EcosystemSidebar from "../../components/ecosystem/EcosystemSidebar";
17+import SpotlightArchive from "../../components/ecosystem/SpotlightArchive";
17import Post from "../../components/ecosystem/Post";
18import Post from "../../components/ecosystem/Post";
18import classnames from "classnames";
19import classnames from "classnames";
19import Link from "next/link";
20import Link from "next/link";
20import fs from "fs";
21import fs from "fs";
21import path from "path";
22import path from "path";
22import Contact from "../../components/Contact";
23import Contact from "../../components/Contact";
24+import { matchEcosystemPost } from "../../lib/lib";
23
25
24export default function Ecosystem({
26export default function Ecosystem({
25 search,
27 search,
@@ -36,10 +38,15 @@ export default function Ecosystem({
36
38
37 const { type } = router.query;
39 const { type } = router.query;
38
40
41+ // Route ?type=archive to below component;
42+ // otherwise, show directory of ecosystem type
43+ // If no param, we show the spotlight for the month
39 if (type === "archive") {
44 if (type === "archive") {
40 return <SpotlightArchive posts={posts} search={search} />;
45 return <SpotlightArchive posts={posts} search={search} />;
41 }
46 }
42
47
48+ const spotlight = type === undefined;
49+
43 let title;
50 let title;
44 if (type) {
51 if (type) {
45 switch (type) {
52 switch (type) {
@@ -91,21 +98,19 @@ export default function Ecosystem({
91 </Sidebar>
98 </Sidebar>
92 <div
99 <div
93 className={classnames("w-full", {
100 className={classnames("w-full", {
94− "max-w-prose": type === undefined,
101+ "max-w-prose": spotlight,
95 })}
102 })}
96 >
103 >
97− {type === undefined && (
104+ {/* Default layout */}
98− <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
105+ {spotlight && (
99− )}
100− {type === undefined && (
101 <>
106 <>
107+ <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
102 <Post post={post} />
108 <Post post={post} />
103 <Link href="/ecosystem/?type=archive" passHref>
109 <Link href="/ecosystem/?type=archive" passHref>
104 <a className="button-lg bg-wall-600 text-white w-fit">
110 <a className="button-lg bg-wall-600 text-white w-fit">
105 Spotlight Archive
111 Spotlight Archive
106 </a>
112 </a>
107 </Link>
113 </Link>
108−
109 <div className="pt-36">
114 <div className="pt-36">
110 <h3 className="pb-2">Urbit Monthly</h3>
115 <h3 className="pb-2">Urbit Monthly</h3>
111 <p className="pb-8">
116 <p className="pb-8">
@@ -116,126 +121,36 @@ export default function Ecosystem({
116 </div>
121 </div>
117 </>
122 </>
118 )}
123 )}
119−
124+ {/* Ecosystem type layouts */}
120 <div
125 <div
121 className={classnames("grid gap-12 w-full", {
126 className={classnames("grid gap-12 w-full", {
122 "grid-cols-2 md:grid-cols-3": type == "organizations" || type == "marketplaces",
127 "grid-cols-2 md:grid-cols-3": type == "organizations" || type == "marketplaces",
123 "grid-cols-1": type === "podcasts" || type === "articles",
128 "grid-cols-1": type === "podcasts" || type === "articles",
124 "grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-2":
129 "grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-2":
125 type === "applications" || type === "groups",
130 type === "applications" || type === "groups",
126− hidden: type === undefined,
131+ hidden: spotlight,
127 })}
132 })}
128 >
133 >
129 {type === "organizations" &&
134 {type === "organizations" &&
130− organizations.map((org) => (
131− <Link href={`/organizations/${org.slug}`}>
135+ <Organizations dir={organizations} />
132− <div className="flex flex-col space-y-4 items-center cursor-pointer hover:opacity-90">
133− <img className="w-36 rounded-xl" src={org.image} />
134− <p className="text-center font-bold">{org.title}</p>
135− </div>
136− </Link>
137− ))}
136+ }
138
137
139 {type === "applications" &&
138 {type === "applications" &&
140− applications.map((app) => (
139+ <Applications dir={applications} />
141− <Link href={`/applications/${app.ship}/${app.slug}`}>
142− <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
143− <div
144− className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
145− style={{
146− backgroundColor: app?.bgColor || "rgba(0,0,0,0)",
147− }}
148− >
149− {app?.image && (
150− <img
151− className="items-center h-36 w-36"
152− src={app.image}
153− />
154− )}
155− </div>
156− <div className="">
157− <p className="text-left font-bold">{app.title}</p>
158− <p className="text-left text-wall-400 font-light">
159− {app.description}
160− </p>
161− </div>
162− </div>
163− </Link>
164− ))}
140+ }
165 {type === "groups" &&
141 {type === "groups" &&
166− groups.map((group) => (
142+ <Groups dir={groups} />
167− <Link href={`/groups/${group.ship}/${group.slug}`}>
168− <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
169− <div
170− className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
171− style={{
172− backgroundColor: group?.tile?.startsWith("#")
173− ? group?.tile
174− : "rgba(0,0,0,0)",
175− }}
176− >
177− {!group?.tile?.startsWith("#") && (
178− <img
179− className="items-center h-36 w-36"
180− src={group.tile}
181− />
182− )}
183− </div>
184− <div className="">
185− <p className="text-left font-bold">{group.title}</p>
186− <p className="text-left text-wall-400 font-light">
187− {group.description}
188− </p>
189− </div>
190− </div>
191− </Link>
192− ))}
143+ }
193 {type === "marketplaces" &&
144 {type === "marketplaces" &&
194− marketplaces.map((market) => (
195− <Link href={`/marketplaces/${market.slug}`}>
145+ <Marketplaces dir={marketplaces} />
196− <div className="flex flex-col space-y-4 justify-center items-center cursor-pointer hover:opacity-90">
197− <img className="w-36 rounded-xl" src={market.image} />
198− <p className="text-center font-bold">{market.title}</p>
199− </div>
200− </Link>
201− ))}
146+ }
202 {type === "podcasts" &&
147 {type === "podcasts" &&
203− podcasts.map((pod) => (
148+ <Podcasts dir={podcasts} />
204− <Link href={`/podcasts/${pod.slug}`}>
205− <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
206− <img className="w-28" src={pod.image} />
207− <div className="flex flex-col space-y-2 min-w-0">
208− <p className="font-bold">{pod.podcast}</p>
209− <p className="min-w-0 min-h-0 leading-5 ">
210− {pod.title}
211− </p>
212− <p className="text-wall-400">
213− {formatDate(generateDisplayDate(pod.date))}
214− </p>
215− </div>
216− </div>
217− </Link>
218− ))}
149+ }
219
150
220 {type === "articles" &&
151 {type === "articles" &&
221− articles.map((article) => (
152+ <Articles dir={articles} />
222− <Link href={`/articles/${article.slug}`}>
153+ }
223− <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
224− <img className="w-28" src={article.image} />
225− <div className="flex flex-col space-y-2 min-w-0">
226− <p className="font-bold">{article.publication}</p>
227− <p className="min-w-0 min-h-0 leading-5 ">
228− {article.title}
229− </p>
230− <p className="text-wall-400">
231− {formatDate(generateDisplayDate(article.date))}
232− </p>
233− </div>
234− </div>
235− </Link>
236− ))}
237−
238−
239 </div>
154 </div>
240 </div>
155 </div>
241 </div>
156 </div>
@@ -246,153 +161,136 @@ export default function Ecosystem({
246 );
161 );
247}
162}
248
163
249−function ActiveLink({ children, href, className, currentPath }) {
164+const Organizations = ({ dir }) => {
250− // special casing this behaviour which overloads one nav
251− const archive = Boolean(
252− currentPath === "/ecosystem?type=archive" && href === "/ecosystem"
253− );
254− const activeClassName = classnames({
255− "text-wall-800": currentPath === href || archive,
256− "text-wall-400": currentPath !== href && !archive,
257− });
258−
259− return (
165+ return dir.map((org) => (
260− <Link href={href} passHref>
166+ <Link href={`/organizations/${org.slug}`}>
261− <a className={`${className} ${activeClassName}`}>{children}</a>
167+ <div className="flex flex-col space-y-4 items-center cursor-pointer hover:opacity-90">
168+ <img className="w-36 rounded-xl" src={org.image} />
169+ <p className="text-center font-bold">{org.title}</p>
170+ </div>
262 </Link>
171 </Link>
263− );
172+ ))
264}
173}
265
174
266−function EcosystemSidebar() {
175+const Applications = ({ dir }) => {
267− const router = useRouter();
268− let currentPath = router.asPath;
269− return (
176+ return dir.map((app) => (
177+ <Link href={`/applications/${app.ship}/${app.slug}`}>
270− <div className="flex flex-col space-y-4 pr-24">
178+ <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
271− <ActiveLink
179+ <div
272− currentPath={currentPath}
180+ className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
273− className="type-ui"
181+ style={{
274− href="/ecosystem"
182+ backgroundColor: app?.bgColor || "rgba(0,0,0,0)",
275− >
183+ }}
276− Spotlight
184+ >
277− </ActiveLink>
185+ {app?.image && (
278− <ActiveLink
186+ <img
279− currentPath={currentPath}
280− className="type-ui"
281− href="/ecosystem?type=organizations"
282− >
283− Organizations
284− </ActiveLink>
285− <ActiveLink
286− currentPath={currentPath}
287− className="type-ui"
187+ className="items-center h-36 w-36"
288− href="/ecosystem?type=applications"
188+ src={app.image}
289− >
189+ />
290− Applications
190+ )}
291− </ActiveLink>
191+ </div>
292− <ActiveLink
293− currentPath={currentPath}
294− className="type-ui"
192+ <div className="">
295− href="/ecosystem?type=groups"
193+ <p className="text-left font-bold">{app.title}</p>
296− >
194+ <p className="text-left text-wall-400 font-light">
297− Groups
195+ {app.description}
298− </ActiveLink>
299− <ActiveLink
300− currentPath={currentPath}
301− className="type-ui"
302− href="/ecosystem?type=marketplaces"
303− >
304− Marketplaces
305− </ActiveLink>
306− <ActiveLink
307− currentPath={currentPath}
308− className="mr-5 type-ui"
309− href="/ecosystem?type=podcasts"
310− >
196+ </p>
311− Podcasts
197+ </div>
312− </ActiveLink>
198+ </div>
199+ </Link>
200+ ))
201+}
313
202
314− <ActiveLink
203+const Groups = ({ dir }) => {
315− currentPath={currentPath}
204+ return dir.map((group) => (
316− className="mr-5 type-ui"
205+ <Link href={`/groups/${group.ship}/${group.slug}`}>
317− href="/ecosystem?type=articles"
206+ <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
207+ <div
208+ className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
209+ style={{
210+ backgroundColor: group?.tile?.startsWith("#")
211+ ? group?.tile
212+ : "rgba(0,0,0,0)",
213+ }}
214+ >
215+ {!group?.tile?.startsWith("#") && (
216+ <img
217+ className="items-center h-36 w-36"
218+ src={group.tile}
219+ />
220+ )}
221+ </div>
222+ <div className="">
223+ <p className="text-left font-bold">{group.title}</p>
224+ <p className="text-left text-wall-400 font-light">
225+ {group.description}
318− >
226+ </p>
319− Articles
227+ </div>
320− </ActiveLink>
228+ </div>
229+ </Link>
230+ ))
231+}
321
232
233+const Marketplaces = ({ dir }) => {
234+ return dir.map((market) => (
235+ <Link href={`/marketplaces/${market.slug}`}>
236+ <div className="flex flex-col space-y-4 justify-center items-center cursor-pointer hover:opacity-90">
237+ <img className="w-36 rounded-xl" src={market.image} />
238+ <p className="text-center font-bold">{market.title}</p>
239+ </div>
240+ </Link>
241+ ))
242+}
322
243
244+const Podcasts = ({ dir }) => {
245+ return dir.map((pod) => (
246+ <Link href={`/podcasts/${pod.slug}`}>
247+ <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
248+ <img className="w-28" src={pod.image} />
249+ <div className="flex flex-col space-y-2 min-w-0">
250+ <p className="font-bold">{pod.podcast}</p>
251+ <p className="min-w-0 min-h-0 leading-5 ">
252+ {pod.title}
253+ </p>
254+ <p className="text-wall-400">
255+ {formatDate(generateDisplayDate(pod.date))}
256+ </p>
257+ </div>
323− </div>
258+ </div>
259+ </Link>
324− );
260+ ))
325}
261}
326
262
327−function SpotlightArchive({ posts, search }) {
328− const post = {
263+const Articles = ({ dir }) => {
329− title: "Spotlight Archive",
264+ return dir.map((article) => (
330− description: "The archive of ecosystem spotlights.",
265+ <Link href={`/articles/${article.slug}`}>
331− };
332− return (
333− <Container>
334− <Head>
335− <title>Archive • Spotlight • Urbit</title>
336− {Meta(post)}
337− </Head>
338− <IntraNav ourSite="https://urbit.org" search={search} />
339− <SingleColumn>
340− <Header search={search} />
341− <Section>
342− <div className="measure">
343− <h1>Ecosystem</h1>
344− </div>
345− </Section>
346− <Section className="flex">
347− <Sidebar>
348− <EcosystemSidebar />
349− </Sidebar>
350− <div>
351− <h3 className="mb-12">Spotlight Archive</h3>
352− {posts.map((post) => {
353− return (
354− <div key={post.slug} className="mb-24 cursor-pointer">
266+ <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
355− <Link href={`/ecosystem/spotlight/${post.slug}`}>
267+ <img className="w-28" src={article.image} />
356− <div>
268+ <div className="flex flex-col space-y-2 min-w-0">
269+ <p className="font-bold">{article.publication}</p>
270+ <p className="min-w-0 min-h-0 leading-5 ">
271+ {article.title}
272+ </p>
357− <h3 className="text-green-400">{post.title}</h3>
273+ <p className="text-wall-400">
358− <div className="type-ui mt-2">
274+ {formatDate(generateDisplayDate(article.date))}
359− {["featured-1", "featured-2", "featured-3"].map(
275+ </p>
360− (feat, i) => {
276+ </div>
361− if (post?.[feat]) {
277+ </div>
362− return (
278+ </Link>
363− <>
364− {i ? ", " : ""}
365− <span>{post[feat].title}</span>
366− </>
367− );
368− }
369− }
370− )}
371− </div>
372− </div>
373− </Link>
374− </div>
375− );
376− })}
377− </div>
378− </Section>
379− </SingleColumn>
380− <Footer />
381− </Container>
382− );
279+ ))
383}
280}
384
281
282+
385−export async function getStaticProps({}) {
283+export async function getStaticProps() {
386 const posts = getAllPosts(
284 const posts = getAllPosts(
387 ["title", "date", "slug", "featured-1", "featured-2", "featured-3"],
285 ["title", "date", "slug", "featured-1", "featured-2", "featured-3"],
388 "ecosystem/spotlight",
286 "ecosystem/spotlight",
389 "date"
287 "date"
390 );
288 );
391− let post = getAllPosts(
289+ const post = matchEcosystemPost(getAllPosts(
392 ["title", "date", "featured-1", "featured-2", "featured-3"],
290 ["title", "date", "featured-1", "featured-2", "featured-3"],
393 "ecosystem/spotlight",
291 "ecosystem/spotlight",
394 "date"
292 "date"
395− )[0];
293+ )[0]);
396 const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
294 const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
397 const podcasts = getAllPosts(
295 const podcasts = getAllPosts(
398 ["title", "image", "date", "podcast", "slug"],
296 ["title", "image", "date", "podcast", "slug"],
@@ -447,27 +345,6 @@ export async function getStaticProps({}) {
447 return nameA < nameB ? -1 : 1;
345 return nameA < nameB ? -1 : 1;
448 });
346 });
449
347
450− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
451− if (post?.[feat]) {
452− const matchedPost = [
453− ...applications.map((e) => ({ ...e, type: "Application" })),
454− ...organizations.map((e) => ({ ...e, type: "Organization" })),
455− ...groups.map((e) => ({ ...e, type: "Group" })),
456− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
457− ...articles.map((e) => ({ ...e, type: "Article" })),
458− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
459− ].filter((e) => e.title === post[feat].title)?.[0];
460− post[feat].image = post[feat]?.image || matchedPost?.image || null;
461− post[feat].type = matchedPost?.type || "Podcast";
462− post[feat].matchedPost = matchedPost || null;
463− post[feat].content = JSON.stringify(
464− Markdown.parse({
465− post: { content: post[feat].content },
466− })
467− );
468− }
469− });
470−
471 return {
348 return {
472 props: {
349 props: {
473 posts,
350 posts,
pages/ecosystem/spotlight/[slug].js+3−61
@@ -6,7 +6,6 @@ import {
6 Container,
6 Container,
7 SingleColumn,
7 SingleColumn,
8 Section,
8 Section,
9− Markdown,
10 IntraNav,
9 IntraNav,
11 getAllPosts,
10 getAllPosts,
12 getPostBySlug,
11 getPostBySlug,
@@ -14,8 +13,7 @@ import {
14import Header from "../../../components/Header";
13import Header from "../../../components/Header";
15import Footer from "../../../components/Footer";
14import Footer from "../../../components/Footer";
16import Post from "../../../components/ecosystem/Post";
15import Post from "../../../components/ecosystem/Post";
17−import fs from "fs";
16+import { matchEcosystemPost } from "../../../lib/lib";
18−import path from "path";
19
17
20export default function BasicPage({ post, markdown, search, index = false }) {
18export default function BasicPage({ post, markdown, search, index = false }) {
21 const router = useRouter();
19 const router = useRouter();
@@ -44,67 +42,11 @@ export default function BasicPage({ post, markdown, search, index = false }) {
44}
42}
45
43
46export async function getStaticProps({ params }) {
44export async function getStaticProps({ params }) {
47− const post = getPostBySlug(
45+ const post = matchEcosystemPost(getPostBySlug(
48 params.slug,
46 params.slug,
49 ["title", "slug", "date", "featured-1", "featured-2", "featured-3"],
47 ["title", "slug", "date", "featured-1", "featured-2", "featured-3"],
50 "ecosystem/spotlight"
48 "ecosystem/spotlight"
51− );
52− const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
53− const groups = getAllPosts(["title", "tile,", "description", "slug"], "groups");
54− const podcasts = getAllPosts(
55− ["title", "image", "date", "podcast", "slug"],
56− "podcasts",
57− "date"
58− );
59− const articles = getAllPosts(
60− ["title", "image", "date", "publication", "author", "slug"],
61− "articles",
62− "date"
63− );
64− const organizations = getAllPosts(
65− ["title", "image", "slug"],
66− "organizations"
67− );
68− const applications = fs
69− .readdirSync(path.join(process.cwd(), "content/applications"), {
70− withFileTypes: true,
71− })
72− .filter((f) => f.isDirectory())
73− .map((dir) =>
74− getAllPosts(
75− ["title", "bgColor", "image", "slug", "description"],
76− `applications/${dir.name}`
77− )
78− .map((e) => ({ ...e, ship: dir.name }))
79− .flat()
80− )
81− .flat()
82− .sort((a, b) => {
83− const nameA = a.title.toLowerCase();
84− const nameB = b.title.toLowerCase();
85− return nameA < nameB ? -1 : 1;
86− });
87−
88− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
89− if (post?.[feat]) {
90− const matchedPost = [
91− ...applications.map((e) => ({ ...e, type: "Application" })),
92− ...organizations.map((e) => ({ ...e, type: "Organization" })),
93− ...groups.map((e) => ({ ...e, type: "Group" })),
94− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
95− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
96− ...articles.map((e) => ({ ...e, type: "Article" })),
97− ].filter((e) => e.title === post[feat].title)?.[0];
98− post[feat].image = post[feat]?.image || matchedPost?.image || null;
99− post[feat].type = matchedPost?.type || "Podcast";
100− post[feat].matchedPost = matchedPost || null;
101− post[feat].content = JSON.stringify(
102− Markdown.parse({
103− post: { content: post[feat].content },
104− })
105− );
106− }
107− });
49+ ));
108
50
109 return {
51 return {
110 props: { post },
52 props: { post },
pages/index.js+4−76
@@ -22,6 +22,7 @@ import IndexCard from "../components/ecosystem/IndexCard";
22import fs from "fs";
22import fs from "fs";
23import path from "path";
23import path from "path";
24import Meta from "../components/Meta";
24import Meta from "../components/Meta";
25+import { matchEcosystemPost } from "../lib/lib";
25
26
26export default function Home({
27export default function Home({
27 posts,
28 posts,
@@ -316,85 +317,12 @@ export async function getStaticProps() {
316 "date"
317 "date"
317 );
318 );
318
319
319− // Pull all latest ecosystem information
320+ // Latest ecosystem report with matched post metadata
320− const ecosystem = getAllPosts(
321+ const ecosystem = matchEcosystemPost(getAllPosts(
321 ["date", "featured-1", "featured-2", "featured-3"],
322 ["date", "featured-1", "featured-2", "featured-3"],
322 "ecosystem/spotlight",
323 "ecosystem/spotlight",
323 "date"
324 "date"
324− )[0];
325+ )[0]);
325−
326− const articles = getAllPosts(
327− ["title", "image", "date", "podcast", "slug"],
328− "articles",
329− "date"
330− );
331−
332− const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
333− const podcasts = getAllPosts(
334− ["title", "image", "date", "podcast", "slug"],
335− "podcasts",
336− "date"
337− );
338− const organizations = getAllPosts(
339− ["title", "image", "slug"],
340− "organizations"
341− );
342− const applications = fs
343− .readdirSync(path.join(process.cwd(), "content/applications"), {
344− withFileTypes: true,
345− })
346− .filter((f) => f.isDirectory())
347− .map((dir) =>
348− getAllPosts(
349− ["title", "bgColor", "image", "slug", "description"],
350− `applications/${dir.name}`
351− )
352− .map((e) => ({ ...e, ship: dir.name }))
353− .flat()
354− )
355− .flat()
356− .sort((a, b) => {
357− const nameA = a.title.toLowerCase();
358− const nameB = b.title.toLowerCase();
359− return nameA < nameB ? -1 : 1;
360− });
361−
362− const groups = fs
363− .readdirSync(path.join(process.cwd(), "content/groups"), {
364− withFileTypes: true,
365− })
366− .filter((f) => f.isDirectory())
367− .map((dir) =>
368− getAllPosts(
369− ["title", "tile", "slug", "description"],
370− `groups/${dir.name}`
371− )
372− .map((e) => ({ ...e, ship: dir.name }))
373− .flat()
374− )
375− .flat()
376− .sort((a, b) => {
377− const nameA = a.title.toLowerCase();
378− const nameB = b.title.toLowerCase();
379− return nameA < nameB ? -1 : 1;
380− });
381−
382− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
383− if (ecosystem?.[feat]) {
384− const matchedPost = [
385− ...articles.map((e) => ({ ...e, type: "Article" })),
386− ...applications.map((e) => ({ ...e, type: "Application" })),
387− ...organizations.map((e) => ({ ...e, type: "Organization" })),
388− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
389− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
390− ...groups.map((e) => ({ ...e, type: "Group" }))
391− ].filter((e) => e.title === ecosystem[feat].title)?.[0];
392− ecosystem[feat].image =
393− ecosystem[feat].image || matchedPost?.image || null;
394− ecosystem[feat].type = matchedPost?.type || "Podcast";
395− ecosystem[feat].matchedPost = matchedPost || null;
396− }
397− });
398
326
399 // Latest grants
327 // Latest grants
400 const basePath = path.join(process.cwd(), 'content/grants')
328 const basePath = path.join(process.cwd(), 'content/grants')