on master help sign in
Merge pull request #1918 from urbit/gc/dec-spot
7596ee3Gordon 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−21
@@ -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,22 +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-wall-600": type === "Podcast",
45− "bg-[#8dd5b1]": type === "Marketplace",
46− "bg-[#B5966D]": type === "Article"
47− }
48− )}
49− >
50− {type}
51− </div>
52− );
53−};
components/ecosystem/Post.js+16−23
@@ -1,10 +1,11 @@
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) => {
6 const childPath =
7 const childPath =
7− post[feat] && post[feat]?.matchedPost?.type === "Application"
8+ post[feat] && (post[feat]?.matchedPost?.type === "Application" || post[feat]?.matchedPost?.type === "Group")
8 ? `${post[feat]?.matchedPost.ship}/${post[feat].matchedPost.slug}`
9 ? `${post[feat]?.matchedPost.ship}/${post[feat].matchedPost.slug}`
9 : `${post[feat].matchedPost?.slug}`;
10 : `${post[feat].matchedPost?.slug}`;
10 const href = post[feat]?.matchedPost
11 const href = post[feat]?.matchedPost
@@ -23,10 +24,10 @@ export default function Post({ post }) {
23 post[feat]?.matchedPost?.bgColor || "rgba(0,0,0,0)",
24 post[feat]?.matchedPost?.bgColor || "rgba(0,0,0,0)",
24 }}
25 }}
25 >
26 >
26− {post[feat]?.matchedPost?.image && (
27+ {(post[feat]?.matchedPost?.image || post[feat]?.matchedPost?.tile) && (
27 <img
28 <img
28 className="rounded-xl w-24 object-cover"
29 className="rounded-xl w-24 object-cover"
29− src={post[feat]?.matchedPost?.image}
30+ src={post[feat]?.matchedPost?.image || post[feat]?.matchedPost?.tile}
30 />
31 />
31 )}
32 )}
32 </div>
33 </div>
@@ -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">
@@ -80,26 +81,18 @@ export default function Post({ post }) {
80 </a>
81 </a>
81 </Link>
82 </Link>
82 )}
83 )}
84+
85+ {post[feat].type === "Group" && (
86+ <Link href={href || "#"} passHref>
87+ <a class="button-sm text-white flex-col bg-green-400 cursor-pointer pr-4 w-fit">
88+ More about {post[feat].title}
89+ </a>
90+ </Link>
91+ )}
92+
83 </div>
93 </div>
84 ) : null;
94 ) : null;
85 });
95 });
86}
96}
87
97
88−const badge = (type) => {
98+
89− return (
90− <div
91− className={classnames(
92− "uppercase font-bold text-white text-xs w-fit rounded-lg mb-2 px-2 py-1",
93− {
94− "bg-[#B37ED7]": type === "Organization",
95− "bg-[#83b2d4]": type === "Application",
96− "bg-wall-600": type === "Podcast",
97− "bg-[#8dd5b1]": type === "Marketplace",
98− "bg-[#B5966D]": type === "Article",
99− }
100− )}
101− >
102− {type}
103− </div>
104− );
105−};
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+}
content/applications/~marnec-dozzod-marzod/groups.mdnew file+12
@@ -0,0 +1,12 @@
1++++
2+title = "Groups (Beta)"
3+shortcode = "~marnec-dozzod-marzod/groups"
4+license = "MIT"
5+image = "https://bootstrap.urbit.org/icon-groups.svg"
6+bgColor = "#EFF0F4"
7+developer = "Tlon"
8+website = "https://github.com/tloncorp/landscape-apps"
9+description = "Groups is a decentralized platform that integrates with Talk, Notebook, and Gallery for a full, communal suite of tools."
10++++
11+
12+Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Groups is a decentralized platform that integrates with Talk, Notebook, and Gallery for a full, communal suite of tools.
content/applications/~tirrel/scene.mdnew file+14
@@ -0,0 +1,14 @@
1++++
2+title = "Scene"
3+shortcode = ""
4+license = "MIT"
5+image = "https://tirrel.io/assets/scene-logo-stroke.svg"
6+bgColor = "rgb(0, 0, 0)"
7+developer = "Tirrel Corp."
8+website = "https://tirrel.io"
9+description = "The new Urbit desktop, by ~tirrel corp."
10++++
11+
12+![Screenshot](https://tirrel.io/assets/scene-demo-screenshot.webp)
13+
14+[Download scene here](https://tirrel.io/scene/index.html).
content/ecosystem/spotlight/2022-december.mdnew file+39
@@ -0,0 +1,39 @@
1++++
2+title = "December 2022"
3+date = "2022-11-29"
4+
5+[featured-1]
6+title = "UF Public"
7+description = "The best place to get the latest updates from the team at the Urbit Foundation."
8+image="https://interstellar.nyc3.digitaloceanspaces.com/battus-datsun/2022.11.07..19.39.22-Sig.png"
9+content = """
10+Join the new and improved Urbit Foundation group to get updates from the Urbit Foundation on developer activity, infrastructure, and general announcements. This group also includes a notebook for available talent as well as a job board.
11+
12+"""
13+
14+[featured-2]
15+title = "Groups (Beta)"
16+description = "The new Groups application from Tlon is officially now in beta."
17+content = """
18+
19+Test drive the future of Urbit now with Tlon's official beta of Groups. This beta features improved discoverability with features like searching by nickname, and creating public, private, or secret groups.
20+
21+Groups is part of the new Landscape suite of applications, and integrates seamlessly with Talk, Gallery, and Notes.
22+
23+Join the [Tlon group](/groups/~nibset-napwyn/tlon-public) for more information.
24+
25+![](https://storage.googleapis.com/media.urbit.org/site/ecosystem/spotlight/groups.jpg)
26+
27+"""
28+
29+[featured-3]
30+
31+title = "Scene"
32+image=""
33+description = "Explore Urbit with tirrel's desktop app, Scene."
34+content = """
35+
36+Tirrel is developing an outside-the-browser Urbit experience geared towards content creators. Check out their vision for Urbit as a desktop application.
37+
38+"""
39++++
content/groups/~nibset-napwyn/tlon-public.md+1−1
@@ -1,6 +1,6 @@
1+++
1+++
2title = "Tlon Public"
2title = "Tlon Public"
3−shortcode = "~nibset-napwyn/tlon-public"
3+shortcode = "~nibset-napwyn/tlon"
4description = "Updates, announcements, and broadcasts from Tlon."
4description = "Updates, announcements, and broadcasts from Tlon."
5type = "Public"
5type = "Public"
6tile = "https://storage.googleapis.com/media.urbit.org/site/ecosystem/organizations/tlon.png"
6tile = "https://storage.googleapis.com/media.urbit.org/site/ecosystem/organizations/tlon.png"
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+141−260
@@ -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,16 +38,24 @@ 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) {
46 case "applications":
53 case "applications":
47 title = "Applications • Ecosystem";
54 title = "Applications • Ecosystem";
48 break;
55 break;
56+ case "applications":
57+ title = "Groups • Ecosystem";
58+ break;
49 case "marketplaces":
59 case "marketplaces":
50 title = "Marketplaces • Ecosystem";
60 title = "Marketplaces • Ecosystem";
51 break;
61 break;
@@ -88,21 +98,19 @@ export default function Ecosystem({
88 </Sidebar>
98 </Sidebar>
89 <div
99 <div
90 className={classnames("w-full", {
100 className={classnames("w-full", {
91− "max-w-prose": type === undefined,
101+ "max-w-prose": spotlight,
92 })}
102 })}
93 >
103 >
94− {type === undefined && (
104+ {/* Default layout */}
95− <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
105+ {spotlight && (
96− )}
97− {type === undefined && (
98 <>
106 <>
107+ <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
99 <Post post={post} />
108 <Post post={post} />
100 <Link href="/ecosystem/?type=archive" passHref>
109 <Link href="/ecosystem/?type=archive" passHref>
101 <a className="button-lg bg-wall-600 text-white w-fit">
110 <a className="button-lg bg-wall-600 text-white w-fit">
102 Spotlight Archive
111 Spotlight Archive
103 </a>
112 </a>
104 </Link>
113 </Link>
105−
106 <div className="pt-36">
114 <div className="pt-36">
107 <h3 className="pb-2">Urbit Monthly</h3>
115 <h3 className="pb-2">Urbit Monthly</h3>
108 <p className="pb-8">
116 <p className="pb-8">
@@ -113,126 +121,36 @@ export default function Ecosystem({
113 </div>
121 </div>
114 </>
122 </>
115 )}
123 )}
116−
124+ {/* Ecosystem type layouts */}
117 <div
125 <div
118 className={classnames("grid gap-12 w-full", {
126 className={classnames("grid gap-12 w-full", {
119 "grid-cols-2 md:grid-cols-3": type == "organizations" || type == "marketplaces",
127 "grid-cols-2 md:grid-cols-3": type == "organizations" || type == "marketplaces",
120 "grid-cols-1": type === "podcasts" || type === "articles",
128 "grid-cols-1": type === "podcasts" || type === "articles",
121 "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":
122 type === "applications" || type === "groups",
130 type === "applications" || type === "groups",
123− hidden: type === undefined,
131+ hidden: spotlight,
124 })}
132 })}
125 >
133 >
126 {type === "organizations" &&
134 {type === "organizations" &&
127− organizations.map((org) => (
128− <Link href={`/organizations/${org.slug}`}>
135+ <Organizations dir={organizations} />
129− <div className="flex flex-col space-y-4 items-center cursor-pointer hover:opacity-90">
130− <img className="w-36 rounded-xl" src={org.image} />
131− <p className="text-center font-bold">{org.title}</p>
132− </div>
133− </Link>
134− ))}
136+ }
135
137
136 {type === "applications" &&
138 {type === "applications" &&
137− applications.map((app) => (
139+ <Applications dir={applications} />
138− <Link href={`/applications/${app.ship}/${app.slug}`}>
139− <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
140− <div
141− className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
142− style={{
143− backgroundColor: app?.bgColor || "rgba(0,0,0,0)",
144− }}
145− >
146− {app?.image && (
147− <img
148− className="items-center h-36 w-36"
149− src={app.image}
150− />
151− )}
152− </div>
153− <div className="">
154− <p className="text-left font-bold">{app.title}</p>
155− <p className="text-left text-wall-400 font-light">
156− {app.description}
157− </p>
158− </div>
159− </div>
160− </Link>
161− ))}
140+ }
162 {type === "groups" &&
141 {type === "groups" &&
163− groups.map((group) => (
142+ <Groups dir={groups} />
164− <Link href={`/groups/${group.ship}/${group.slug}`}>
165− <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
166− <div
167− className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
168− style={{
169− backgroundColor: group?.tile?.startsWith("#")
170− ? group?.tile
171− : "rgba(0,0,0,0)",
172− }}
173− >
174− {!group?.tile?.startsWith("#") && (
175− <img
176− className="items-center h-36 w-36"
177− src={group.tile}
178− />
179− )}
180− </div>
181− <div className="">
182− <p className="text-left font-bold">{group.title}</p>
183− <p className="text-left text-wall-400 font-light">
184− {group.description}
185− </p>
186− </div>
187− </div>
188− </Link>
189− ))}
143+ }
190 {type === "marketplaces" &&
144 {type === "marketplaces" &&
191− marketplaces.map((market) => (
192− <Link href={`/marketplaces/${market.slug}`}>
145+ <Marketplaces dir={marketplaces} />
193− <div className="flex flex-col space-y-4 justify-center items-center cursor-pointer hover:opacity-90">
194− <img className="w-36 rounded-xl" src={market.image} />
195− <p className="text-center font-bold">{market.title}</p>
196− </div>
197− </Link>
198− ))}
146+ }
199 {type === "podcasts" &&
147 {type === "podcasts" &&
200− podcasts.map((pod) => (
148+ <Podcasts dir={podcasts} />
201− <Link href={`/podcasts/${pod.slug}`}>
202− <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
203− <img className="w-28" src={pod.image} />
204− <div className="flex flex-col space-y-2 min-w-0">
205− <p className="font-bold">{pod.podcast}</p>
206− <p className="min-w-0 min-h-0 leading-5 ">
207− {pod.title}
208− </p>
209− <p className="text-wall-400">
210− {formatDate(generateDisplayDate(pod.date))}
211− </p>
212− </div>
213− </div>
214− </Link>
215− ))}
149+ }
216
150
217 {type === "articles" &&
151 {type === "articles" &&
218− articles.map((article) => (
152+ <Articles dir={articles} />
219− <Link href={`/articles/${article.slug}`}>
153+ }
220− <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
221− <img className="w-28" src={article.image} />
222− <div className="flex flex-col space-y-2 min-w-0">
223− <p className="font-bold">{article.publication}</p>
224− <p className="min-w-0 min-h-0 leading-5 ">
225− {article.title}
226− </p>
227− <p className="text-wall-400">
228− {formatDate(generateDisplayDate(article.date))}
229− </p>
230− </div>
231− </div>
232− </Link>
233− ))}
234−
235−
236 </div>
154 </div>
237 </div>
155 </div>
238 </div>
156 </div>
@@ -243,153 +161,136 @@ export default function Ecosystem({
243 );
161 );
244}
162}
245
163
246−function ActiveLink({ children, href, className, currentPath }) {
164+const Organizations = ({ dir }) => {
247− // special casing this behaviour which overloads one nav
248− const archive = Boolean(
249− currentPath === "/ecosystem?type=archive" && href === "/ecosystem"
250− );
251− const activeClassName = classnames({
252− "text-wall-800": currentPath === href || archive,
253− "text-wall-400": currentPath !== href && !archive,
254− });
255−
256− return (
165+ return dir.map((org) => (
257− <Link href={href} passHref>
166+ <Link href={`/organizations/${org.slug}`}>
258− <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>
259 </Link>
171 </Link>
260− );
172+ ))
261}
173}
262
174
263−function EcosystemSidebar() {
175+const Applications = ({ dir }) => {
264− const router = useRouter();
265− let currentPath = router.asPath;
266− return (
176+ return dir.map((app) => (
177+ <Link href={`/applications/${app.ship}/${app.slug}`}>
267− <div className="flex flex-col space-y-4 pr-24">
178+ <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
268− <ActiveLink
179+ <div
269− currentPath={currentPath}
180+ className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
270− className="type-ui"
181+ style={{
271− href="/ecosystem"
182+ backgroundColor: app?.bgColor || "rgba(0,0,0,0)",
272− >
183+ }}
273− Spotlight
184+ >
274− </ActiveLink>
185+ {app?.image && (
275− <ActiveLink
186+ <img
276− currentPath={currentPath}
277− className="type-ui"
278− href="/ecosystem?type=organizations"
279− >
280− Organizations
281− </ActiveLink>
282− <ActiveLink
283− currentPath={currentPath}
284− className="type-ui"
187+ className="items-center h-36 w-36"
285− href="/ecosystem?type=applications"
188+ src={app.image}
286− >
189+ />
287− Applications
190+ )}
288− </ActiveLink>
191+ </div>
289− <ActiveLink
290− currentPath={currentPath}
291− className="type-ui"
192+ <div className="">
292− href="/ecosystem?type=groups"
193+ <p className="text-left font-bold">{app.title}</p>
293− >
194+ <p className="text-left text-wall-400 font-light">
294− Groups
195+ {app.description}
295− </ActiveLink>
296− <ActiveLink
297− currentPath={currentPath}
298− className="type-ui"
299− href="/ecosystem?type=marketplaces"
300− >
301− Marketplaces
302− </ActiveLink>
303− <ActiveLink
304− currentPath={currentPath}
305− className="mr-5 type-ui"
306− href="/ecosystem?type=podcasts"
307− >
196+ </p>
308− Podcasts
197+ </div>
309− </ActiveLink>
198+ </div>
199+ </Link>
200+ ))
201+}
310
202
311− <ActiveLink
203+const Groups = ({ dir }) => {
312− currentPath={currentPath}
204+ return dir.map((group) => (
313− className="mr-5 type-ui"
205+ <Link href={`/groups/${group.ship}/${group.slug}`}>
314− 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}
315− >
226+ </p>
316− Articles
227+ </div>
317− </ActiveLink>
228+ </div>
229+ </Link>
230+ ))
231+}
318
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+}
319
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>
320− </div>
258+ </div>
259+ </Link>
321− );
260+ ))
322}
261}
323
262
324−function SpotlightArchive({ posts, search }) {
325− const post = {
263+const Articles = ({ dir }) => {
326− title: "Spotlight Archive",
264+ return dir.map((article) => (
327− description: "The archive of ecosystem spotlights.",
265+ <Link href={`/articles/${article.slug}`}>
328− };
329− return (
330− <Container>
331− <Head>
332− <title>Archive • Spotlight • Urbit</title>
333− {Meta(post)}
334− </Head>
335− <IntraNav ourSite="https://urbit.org" search={search} />
336− <SingleColumn>
337− <Header search={search} />
338− <Section>
339− <div className="measure">
340− <h1>Ecosystem</h1>
341− </div>
342− </Section>
343− <Section className="flex">
344− <Sidebar>
345− <EcosystemSidebar />
346− </Sidebar>
347− <div>
348− <h3 className="mb-12">Spotlight Archive</h3>
349− {posts.map((post) => {
350− return (
351− <div key={post.slug} className="mb-24 cursor-pointer">
266+ <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
352− <Link href={`/ecosystem/spotlight/${post.slug}`}>
267+ <img className="w-28" src={article.image} />
353− <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>
354− <h3 className="text-green-400">{post.title}</h3>
273+ <p className="text-wall-400">
355− <div className="type-ui mt-2">
274+ {formatDate(generateDisplayDate(article.date))}
356− {["featured-1", "featured-2", "featured-3"].map(
275+ </p>
357− (feat, i) => {
276+ </div>
358− if (post?.[feat]) {
277+ </div>
359− return (
278+ </Link>
360− <>
361− {i ? ", " : ""}
362− <span>{post[feat].title}</span>
363− </>
364− );
365− }
366− }
367− )}
368− </div>
369− </div>
370− </Link>
371− </div>
372− );
373− })}
374− </div>
375− </Section>
376− </SingleColumn>
377− <Footer />
378− </Container>
379− );
279+ ))
380}
280}
381
281
282+
382−export async function getStaticProps({}) {
283+export async function getStaticProps() {
383 const posts = getAllPosts(
284 const posts = getAllPosts(
384 ["title", "date", "slug", "featured-1", "featured-2", "featured-3"],
285 ["title", "date", "slug", "featured-1", "featured-2", "featured-3"],
385 "ecosystem/spotlight",
286 "ecosystem/spotlight",
386 "date"
287 "date"
387 );
288 );
388− let post = getAllPosts(
289+ const post = matchEcosystemPost(getAllPosts(
389 ["title", "date", "featured-1", "featured-2", "featured-3"],
290 ["title", "date", "featured-1", "featured-2", "featured-3"],
390 "ecosystem/spotlight",
291 "ecosystem/spotlight",
391 "date"
292 "date"
392− )[0];
293+ )[0]);
393 const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
294 const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
394 const podcasts = getAllPosts(
295 const podcasts = getAllPosts(
395 ["title", "image", "date", "podcast", "slug"],
296 ["title", "image", "date", "podcast", "slug"],
@@ -444,26 +345,6 @@ export async function getStaticProps({}) {
444 return nameA < nameB ? -1 : 1;
345 return nameA < nameB ? -1 : 1;
445 });
346 });
446
347
447− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
448− if (post?.[feat]) {
449− const matchedPost = [
450− ...applications.map((e) => ({ ...e, type: "Application" })),
451− ...organizations.map((e) => ({ ...e, type: "Organization" })),
452− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
453− ...articles.map((e) => ({ ...e, type: "Article" })),
454− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
455− ].filter((e) => e.title === post[feat].title)?.[0];
456− post[feat].image = post[feat]?.image || matchedPost?.image || null;
457− post[feat].type = matchedPost?.type || "Podcast";
458− post[feat].matchedPost = matchedPost || null;
459− post[feat].content = JSON.stringify(
460− Markdown.parse({
461− post: { content: post[feat].content },
462− })
463− );
464− }
465− });
466−
467 return {
348 return {
468 props: {
349 props: {
469 posts,
350 posts,
pages/ecosystem/spotlight/[slug].js+3−59
@@ -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,65 +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 podcasts = getAllPosts(
54− ["title", "image", "date", "podcast", "slug"],
55− "podcasts",
56− "date"
57− );
58− const articles = getAllPosts(
59− ["title", "image", "date", "publication", "author", "slug"],
60− "articles",
61− "date"
62− );
63− const organizations = getAllPosts(
64− ["title", "image", "slug"],
65− "organizations"
66− );
67− const applications = fs
68− .readdirSync(path.join(process.cwd(), "content/applications"), {
69− withFileTypes: true,
70− })
71− .filter((f) => f.isDirectory())
72− .map((dir) =>
73− getAllPosts(
74− ["title", "bgColor", "image", "slug", "description"],
75− `applications/${dir.name}`
76− )
77− .map((e) => ({ ...e, ship: dir.name }))
78− .flat()
79− )
80− .flat()
81− .sort((a, b) => {
82− const nameA = a.title.toLowerCase();
83− const nameB = b.title.toLowerCase();
84− return nameA < nameB ? -1 : 1;
85− });
86−
87− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
88− if (post?.[feat]) {
89− const matchedPost = [
90− ...applications.map((e) => ({ ...e, type: "Application" })),
91− ...organizations.map((e) => ({ ...e, type: "Organization" })),
92− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
93− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
94− ...articles.map((e) => ({ ...e, type: "Article" })),
95− ].filter((e) => e.title === post[feat].title)?.[0];
96− post[feat].image = post[feat]?.image || matchedPost?.image || null;
97− post[feat].type = matchedPost?.type || "Podcast";
98− post[feat].matchedPost = matchedPost || null;
99− post[feat].content = JSON.stringify(
100− Markdown.parse({
101− post: { content: post[feat].content },
102− })
103− );
104− }
105− });
49+ ));
106
50
107 return {
51 return {
108 props: { post },
52 props: { post },
pages/index.js+4−55
@@ -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,64 +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− ["featured-1", "featured-2", "featured-3"].forEach((feat) => {
363− if (ecosystem?.[feat]) {
364− const matchedPost = [
365− ...articles.map((e) => ({ ...e, type: "Article" })),
366− ...applications.map((e) => ({ ...e, type: "Application" })),
367− ...organizations.map((e) => ({ ...e, type: "Organization" })),
368− ...podcasts.map((e) => ({ ...e, type: "Podcast" })),
369− ...marketplaces.map((e) => ({ ...e, type: "Marketplace" })),
370− ].filter((e) => e.title === ecosystem[feat].title)?.[0];
371− ecosystem[feat].image =
372− ecosystem[feat].image || matchedPost?.image || null;
373− ecosystem[feat].type = matchedPost?.type || "Podcast";
374− ecosystem[feat].matchedPost = matchedPost || null;
375− }
376− });
377
326
378 // Latest grants
327 // Latest grants
379 const basePath = path.join(process.cwd(), 'content/grants')
328 const basePath = path.join(process.cwd(), 'content/grants')