on master help sign in
Merge pull request #1918 from urbit/gc/dec-spot

dec spotlight, groups functionality in spotlight

commit7596ee37f41bcf828a4a820e4ef8dce7532fa22a authorGordon Cieplak <gordon@urbit.org> dateTue 29 Nov 2022, 21:23 (4 years ago) parent4c31277d86460c
13 files changed+480−420
components/ecosystem/Badge.js+20 components/ecosystem/EcosystemSidebar.js+78 components/ecosystem/IndexCard.js+2−21 components/ecosystem/Post.js+16−23 components/ecosystem/SpotlightArchive.js+66 content/applications/~marnec-dozzod-marzod/groups.md+12 content/applications/~tirrel/scene.md+14 content/ecosystem/spotlight/2022-december.md+39 content/groups/~nibset-napwyn/tlon-public.md+1−1 lib/lib.js+84−1 pages/ecosystem/index.js+141−260 pages/ecosystem/spotlight/[slug].js+3−59 pages/index.js+4−55
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 @@
11import Link from "next/link";
2−import classnames from "classnames";
2+import EcosystemBadge from "./Badge";
33export default function IndexCard({ slug, feat }) {
44 return (
55 <Link href={slug}>
@@ -24,7 +24,7 @@ export default function IndexCard({ slug, feat }) {
2424 )}
2525 <div className="pt-4">
2626 <p className="mb-2 font-semibold leading-5">{feat.title}</p>
27− {badge(feat.type)}
27+ <EcosystemBadge type={feat.type} />
2828 <p className="text-sm leading-5">{feat?.description}</p>
2929 </div>
3030 </div>
@@ -32,22 +32,3 @@ export default function IndexCard({ slug, feat }) {
3232 </Link>
3333 );
3434}
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 @@
11import { Markdown } from "@urbit/foundation-design-system";
2−import classnames from "classnames";
2+import EcosystemBadge from "./Badge";
3+
34import Link from "next/link";
45export default function Post({ post }) {
56 return ["featured-1", "featured-2", "featured-3"].map((feat) => {
67 const childPath =
7− post[feat] && post[feat]?.matchedPost?.type === "Application"
8+ post[feat] && (post[feat]?.matchedPost?.type === "Application" || post[feat]?.matchedPost?.type === "Group")
89 ? `${post[feat]?.matchedPost.ship}/${post[feat].matchedPost.slug}`
910 : `${post[feat].matchedPost?.slug}`;
1011 const href = post[feat]?.matchedPost
@@ -23,10 +24,10 @@ export default function Post({ post }) {
2324 post[feat]?.matchedPost?.bgColor || "rgba(0,0,0,0)",
2425 }}
2526 >
26− {post[feat]?.matchedPost?.image && (
27+ {(post[feat]?.matchedPost?.image || post[feat]?.matchedPost?.tile) && (
2728 <img
2829 className="rounded-xl w-24 object-cover"
29− src={post[feat]?.matchedPost?.image}
30+ src={post[feat]?.matchedPost?.image || post[feat]?.matchedPost?.tile}
3031 />
3132 )}
3233 </div>
@@ -34,7 +35,7 @@ export default function Post({ post }) {
3435 <h4 className="text-xl pb-2 leading-6 font-bold">
3536 {post?.[feat].title}
3637 </h4>
37− {badge(post[feat].type)}
38+ <EcosystemBadge type={post[feat].type} />
3839 </div>
3940 </div>
4041 <div className="markdown mt-8 mb-8">
@@ -80,26 +81,18 @@ export default function Post({ post }) {
8081 </a>
8182 </Link>
8283 )}
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+
8393 </div>
8494 ) : null;
8595 });
8696}
8797
88−const badge = (type) => {
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−};
98+
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 @@
11+++
22title = "Tlon Public"
3−shortcode = "~nibset-napwyn/tlon-public"
3+shortcode = "~nibset-napwyn/tlon"
44description = "Updates, announcements, and broadcasts from Tlon."
55type = "Public"
66tile = "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";
33import matter from "gray-matter";
44import toml from "@iarna/toml";
55import { DateTime } from "luxon";
6−import { getPostBySlug, getPostSlugs } from "@urbit/foundation-design-system";
6+import { Markdown, getPostBySlug, getPostSlugs } from "@urbit/foundation-design-system";
77
88// NB Gavin: This could be much simpler if we converted everything to a more common standard, like YAML
99const options = {
@@ -166,3 +166,86 @@ export const pair = (arr) =>
166166 rows
167167 );
168168 }, []);
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 {
66 SingleColumn,
77 Section,
88 IntraNav,
9− Markdown,
109 getAllPosts,
1110 generateDisplayDate,
1211 formatDate,
@@ -14,12 +13,15 @@ import {
1413import Header from "../../components/Header";
1514import Footer from "../../components/Footer";
1615import Sidebar from "../../components/Sidebar";
16+import EcosystemSidebar from "../../components/ecosystem/EcosystemSidebar";
17+import SpotlightArchive from "../../components/ecosystem/SpotlightArchive";
1718import Post from "../../components/ecosystem/Post";
1819import classnames from "classnames";
1920import Link from "next/link";
2021import fs from "fs";
2122import path from "path";
2223import Contact from "../../components/Contact";
24+import { matchEcosystemPost } from "../../lib/lib";
2325
2426export default function Ecosystem({
2527 search,
@@ -36,16 +38,24 @@ export default function Ecosystem({
3638
3739 const { type } = router.query;
3840
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
3944 if (type === "archive") {
4045 return <SpotlightArchive posts={posts} search={search} />;
4146 }
4247
48+ const spotlight = type === undefined;
49+
4350 let title;
4451 if (type) {
4552 switch (type) {
4653 case "applications":
4754 title = "Applications • Ecosystem";
4855 break;
56+ case "applications":
57+ title = "Groups • Ecosystem";
58+ break;
4959 case "marketplaces":
5060 title = "Marketplaces • Ecosystem";
5161 break;
@@ -88,21 +98,19 @@ export default function Ecosystem({
8898 </Sidebar>
8999 <div
90100 className={classnames("w-full", {
91− "max-w-prose": type === undefined,
101+ "max-w-prose": spotlight,
92102 })}
93103 >
94− {type === undefined && (
95− <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
96− )}
97− {type === undefined && (
104+ {/* Default layout */}
105+ {spotlight && (
98106 <>
107+ <h3>{`Ecosystem Spotlight - ${post.title}`}</h3>
99108 <Post post={post} />
100109 <Link href="/ecosystem/?type=archive" passHref>
101110 <a className="button-lg bg-wall-600 text-white w-fit">
102111 Spotlight Archive
103112 </a>
104113 </Link>
105−
106114 <div className="pt-36">
107115 <h3 className="pb-2">Urbit Monthly</h3>
108116 <p className="pb-8">
@@ -113,126 +121,36 @@ export default function Ecosystem({
113121 </div>
114122 </>
115123 )}
116−
124+ {/* Ecosystem type layouts */}
117125 <div
118126 className={classnames("grid gap-12 w-full", {
119127 "grid-cols-2 md:grid-cols-3": type == "organizations" || type == "marketplaces",
120128 "grid-cols-1": type === "podcasts" || type === "articles",
121129 "grid-cols-1 md:grid-cols-1 lg:grid-cols-1 xl:grid-cols-2":
122130 type === "applications" || type === "groups",
123− hidden: type === undefined,
131+ hidden: spotlight,
124132 })}
125133 >
126134 {type === "organizations" &&
127− organizations.map((org) => (
128− <Link href={`/organizations/${org.slug}`}>
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− ))}
135+ <Organizations dir={organizations} />
136+ }
135137
136138 {type === "applications" &&
137− applications.map((app) => (
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− ))}
139+ <Applications dir={applications} />
140+ }
162141 {type === "groups" &&
163− groups.map((group) => (
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− ))}
142+ <Groups dir={groups} />
143+ }
190144 {type === "marketplaces" &&
191− marketplaces.map((market) => (
192− <Link href={`/marketplaces/${market.slug}`}>
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− ))}
145+ <Marketplaces dir={marketplaces} />
146+ }
199147 {type === "podcasts" &&
200− podcasts.map((pod) => (
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− ))}
148+ <Podcasts dir={podcasts} />
149+ }
216150
217151 {type === "articles" &&
218− articles.map((article) => (
219− <Link href={`/articles/${article.slug}`}>
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−
152+ <Articles dir={articles} />
153+ }
236154 </div>
237155 </div>
238156 </div>
@@ -243,153 +161,136 @@ export default function Ecosystem({
243161 );
244162}
245163
246−function ActiveLink({ children, href, className, currentPath }) {
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 (
257− <Link href={href} passHref>
258− <a className={`${className} ${activeClassName}`}>{children}</a>
164+const Organizations = ({ dir }) => {
165+ return dir.map((org) => (
166+ <Link href={`/organizations/${org.slug}`}>
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>
259171 </Link>
260− );
172+ ))
261173}
262174
263−function EcosystemSidebar() {
264− const router = useRouter();
265− let currentPath = router.asPath;
266− return (
267− <div className="flex flex-col space-y-4 pr-24">
268− <ActiveLink
269− currentPath={currentPath}
270− className="type-ui"
271− href="/ecosystem"
272− >
273− Spotlight
274− </ActiveLink>
275− <ActiveLink
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"
285− href="/ecosystem?type=applications"
286− >
287− Applications
288− </ActiveLink>
289− <ActiveLink
290− currentPath={currentPath}
291− className="type-ui"
292− href="/ecosystem?type=groups"
293− >
294− Groups
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− >
308− Podcasts
309− </ActiveLink>
175+const Applications = ({ dir }) => {
176+ return dir.map((app) => (
177+ <Link href={`/applications/${app.ship}/${app.slug}`}>
178+ <div className="flex space-x-4 items-center cursor-pointer hover:opacity-90">
179+ <div
180+ className="h-36 w-36 rounded-xl items-center justify-center shrink-0 overflow-hidden"
181+ style={{
182+ backgroundColor: app?.bgColor || "rgba(0,0,0,0)",
183+ }}
184+ >
185+ {app?.image && (
186+ <img
187+ className="items-center h-36 w-36"
188+ src={app.image}
189+ />
190+ )}
191+ </div>
192+ <div className="">
193+ <p className="text-left font-bold">{app.title}</p>
194+ <p className="text-left text-wall-400 font-light">
195+ {app.description}
196+ </p>
197+ </div>
198+ </div>
199+ </Link>
200+ ))
201+}
310202
311− <ActiveLink
312− currentPath={currentPath}
313− className="mr-5 type-ui"
314− href="/ecosystem?type=articles"
315− >
316− Articles
317− </ActiveLink>
203+const Groups = ({ dir }) => {
204+ return dir.map((group) => (
205+ <Link href={`/groups/${group.ship}/${group.slug}`}>
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}
226+ </p>
227+ </div>
228+ </div>
229+ </Link>
230+ ))
231+}
318232
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+}
319243
320− </div>
321− );
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>
258+ </div>
259+ </Link>
260+ ))
322261}
323262
324−function SpotlightArchive({ posts, search }) {
325− const post = {
326− title: "Spotlight Archive",
327− description: "The archive of ecosystem spotlights.",
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">
352− <Link href={`/ecosystem/spotlight/${post.slug}`}>
353− <div>
354− <h3 className="text-green-400">{post.title}</h3>
355− <div className="type-ui mt-2">
356− {["featured-1", "featured-2", "featured-3"].map(
357− (feat, i) => {
358− if (post?.[feat]) {
359− return (
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− );
263+const Articles = ({ dir }) => {
264+ return dir.map((article) => (
265+ <Link href={`/articles/${article.slug}`}>
266+ <div className="flex cursor-pointer space-x-4 items-center hover:opacity-90">
267+ <img className="w-28" src={article.image} />
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>
273+ <p className="text-wall-400">
274+ {formatDate(generateDisplayDate(article.date))}
275+ </p>
276+ </div>
277+ </div>
278+ </Link>
279+ ))
380280}
381281
382−export async function getStaticProps({}) {
282+
283+export async function getStaticProps() {
383284 const posts = getAllPosts(
384285 ["title", "date", "slug", "featured-1", "featured-2", "featured-3"],
385286 "ecosystem/spotlight",
386287 "date"
387288 );
388− let post = getAllPosts(
289+ const post = matchEcosystemPost(getAllPosts(
389290 ["title", "date", "featured-1", "featured-2", "featured-3"],
390291 "ecosystem/spotlight",
391292 "date"
392− )[0];
293+ )[0]);
393294 const marketplaces = getAllPosts(["title", "image", "slug"], "marketplaces");
394295 const podcasts = getAllPosts(
395296 ["title", "image", "date", "podcast", "slug"],
@@ -444,26 +345,6 @@ export async function getStaticProps({}) {
444345 return nameA < nameB ? -1 : 1;
445346 });
446347
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−
467348 return {
468349 props: {
469350 posts,
pages/ecosystem/spotlight/[slug].js+3−59
@@ -6,7 +6,6 @@ import {
66 Container,
77 SingleColumn,
88 Section,
9− Markdown,
109 IntraNav,
1110 getAllPosts,
1211 getPostBySlug,
@@ -14,8 +13,7 @@ import {
1413import Header from "../../../components/Header";
1514import Footer from "../../../components/Footer";
1615import Post from "../../../components/ecosystem/Post";
17−import fs from "fs";
18−import path from "path";
16+import { matchEcosystemPost } from "../../../lib/lib";
1917
2018export default function BasicPage({ post, markdown, search, index = false }) {
2119 const router = useRouter();
@@ -44,65 +42,11 @@ export default function BasicPage({ post, markdown, search, index = false }) {
4442}
4543
4644export async function getStaticProps({ params }) {
47− const post = getPostBySlug(
45+ const post = matchEcosystemPost(getPostBySlug(
4846 params.slug,
4947 ["title", "slug", "date", "featured-1", "featured-2", "featured-3"],
5048 "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+ ));
10650
10751 return {
10852 props: { post },
pages/index.js+4−55
@@ -22,6 +22,7 @@ import IndexCard from "../components/ecosystem/IndexCard";
2222import fs from "fs";
2323import path from "path";
2424import Meta from "../components/Meta";
25+import { matchEcosystemPost } from "../lib/lib";
2526
2627export default function Home({
2728 posts,
@@ -316,64 +317,12 @@ export async function getStaticProps() {
316317 "date"
317318 );
318319
319− // Pull all latest ecosystem information
320− const ecosystem = getAllPosts(
320+ // Latest ecosystem report with matched post metadata
321+ const ecosystem = matchEcosystemPost(getAllPosts(
321322 ["date", "featured-1", "featured-2", "featured-3"],
322323 "ecosystem/spotlight",
323324 "date"
324− )[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− });
325+ )[0]);
377326
378327 // Latest grants
379328 const basePath = path.join(process.cwd(), 'content/grants')