@grove-dev/astro 0.2.10 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/display.d.ts +11 -17
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +43 -43
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +6 -7
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +11 -12
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +35 -73
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +144 -126
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +11 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +12 -25
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +15 -16
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +4 -11
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +47 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +195 -180
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +7 -21
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +12 -31
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +4 -4
- package/src/components/AppsIndexRow.astro +55 -184
- package/src/components/AppsPagination.astro +45 -108
- package/src/components/CategoryGrid.astro +43 -31
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +41 -0
- package/src/components/ExploreByStack.astro +6 -0
- package/src/components/FilterGroupMenu.astro +49 -0
- package/src/components/FilterOptions.astro +44 -0
- package/src/components/Hero.astro +198 -147
- package/src/components/Icon.astro +10 -3
- package/src/components/ItemCard.astro +266 -248
- package/src/components/RecordSection.astro +68 -33
- package/src/components/RefinePanel.astro +115 -141
- package/src/components/StackGrid.astro +32 -24
- package/src/components/WhyThisExists.astro +32 -32
- package/src/layouts/BaseLayout.astro +104 -49
- package/src/layouts/Container.astro +5 -44
- package/src/layouts/Footer.astro +31 -20
- package/src/layouts/Header.astro +40 -19
- package/src/layouts/SectionHeader.astro +27 -43
- package/src/layouts/Seo.astro +38 -56
- package/src/layouts/ThemeToggle.astro +35 -48
- package/src/lib/display.ts +46 -53
- package/src/lib/format.ts +11 -12
- package/src/lib/lenses.ts +151 -172
- package/src/lib/repo.ts +16 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +222 -290
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +207 -1
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
- package/templates/default/.github/pull_request_template.md +27 -0
- package/templates/default/.github/workflows/build.yml +55 -0
- package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
- package/templates/default/.github/workflows/daily-refresh.yml +53 -0
- package/templates/default/.github/workflows/sync-contributors.yml +53 -0
- package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
- package/templates/default/.github/workflows/validate-data.yml +35 -0
- package/templates/default/data/records/coolify.yml +46 -0
- package/templates/default/data/records/gitea.yml +46 -0
- package/templates/default/data/records/hoppscotch.yml +47 -0
- package/templates/default/data/records/inventree.yml +47 -0
- package/templates/default/data/records/label-studio.yml +46 -0
- package/templates/default/data/records/logseq.yml +48 -0
- package/templates/default/data/records/private-gpt.yml +47 -0
- package/templates/default/data/records/wakapi.yml +44 -0
- package/templates/default/data/taxonomy/categories.yml +47 -0
- package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
- package/templates/default/data/taxonomy/platforms.yml +27 -0
- package/templates/default/data/taxonomy/stacks.yml +41 -0
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +5 -4
- package/templates/default/public/icons/stacks/bun.svg +1 -0
- package/templates/default/public/icons/stacks/clojure.svg +1 -0
- package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
- package/templates/default/public/icons/stacks/deno.svg +1 -0
- package/templates/default/public/icons/stacks/django.svg +1 -0
- package/templates/default/public/icons/stacks/docker.svg +1 -0
- package/templates/default/public/icons/stacks/go.svg +1 -0
- package/templates/default/public/icons/stacks/llm.svg +1 -0
- package/templates/default/public/icons/stacks/node.js.svg +1 -0
- package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
- package/templates/default/public/icons/stacks/vue.svg +1 -0
- package/templates/default/public/llms-full.txt +197 -5
- package/templates/default/public/llms.txt +107 -6
- package/templates/default/public/sitemap.xml +63 -0
- package/templates/default/scripts/build-llms.mjs +123 -0
- package/templates/default/scripts/build-records-json.mjs +27 -0
- package/templates/default/scripts/build-sitemap.mjs +20 -0
- package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
- package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
- package/templates/default/scripts/fetch-icons.mjs +137 -0
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
- package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
- package/templates/default/scripts/refresh-records-activity.mjs +24 -0
- package/templates/default/scripts/repair-license-format.mjs +72 -0
- package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
- package/templates/default/scripts/seed-from-github.mjs +62 -0
- package/templates/default/scripts/sync-contributors.mjs +145 -0
- package/templates/default/scripts/sync-github-metadata.mjs +28 -0
- package/templates/default/scripts/validate-records.mjs +27 -0
- package/templates/default/src/data/records.ts +134 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +476 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
- package/templates/default/src/pages/apps/index.astro +146 -0
- package/templates/default/src/pages/contributors.astro +184 -0
- package/templates/default/src/pages/index.astro +119 -162
- package/templates/default/src/pages/submit.astro +276 -114
- package/templates/default/wrangler.jsonc +28 -0
- package/LICENSE +0 -21
- package/templates/default/.astro/content.d.ts +0 -155
- package/templates/default/.astro/settings.json +0 -5
- package/templates/default/.astro/types.d.ts +0 -1
- package/templates/default/data/generated/apps.json +0 -1
- package/templates/default/data/generated/records.full.json +0 -8
- package/templates/default/data/generated/records.index.json +0 -7
- package/templates/default/data/generated/records.json +0 -8
- package/templates/default/data/generated/repo-stats.json +0 -1
- package/templates/default/data/generated/site-config.json +0 -31
- package/templates/default/dist/_astro/BaseLayout.Dz_HR-68.css +0 -1
- package/templates/default/dist/about/index.html +0 -165
- package/templates/default/dist/icons/platforms/android.svg +0 -1
- package/templates/default/dist/icons/platforms/apple-dark.svg +0 -1
- package/templates/default/dist/icons/platforms/apple-light.svg +0 -1
- package/templates/default/dist/icons/platforms/chrome.svg +0 -1
- package/templates/default/dist/icons/platforms/chromeos.svg +0 -5
- package/templates/default/dist/icons/platforms/desktop.svg +0 -4
- package/templates/default/dist/icons/platforms/embedded.svg +0 -5
- package/templates/default/dist/icons/platforms/ios.svg +0 -1
- package/templates/default/dist/icons/platforms/linux.svg +0 -1
- package/templates/default/dist/icons/platforms/macos.svg +0 -1
- package/templates/default/dist/icons/platforms/ubuntu.svg +0 -1
- package/templates/default/dist/icons/platforms/web.svg +0 -5
- package/templates/default/dist/icons/platforms/windows.svg +0 -6
- package/templates/default/dist/icons/stacks/android.svg +0 -1
- package/templates/default/dist/icons/stacks/apple-dark.svg +0 -1
- package/templates/default/dist/icons/stacks/apple-light.svg +0 -1
- package/templates/default/dist/icons/stacks/capacitor.svg +0 -14
- package/templates/default/dist/icons/stacks/dart.svg +0 -1
- package/templates/default/dist/icons/stacks/firebase.svg +0 -1
- package/templates/default/dist/icons/stacks/flutter.svg +0 -1
- package/templates/default/dist/icons/stacks/graphql.svg +0 -1
- package/templates/default/dist/icons/stacks/ionic.svg +0 -4
- package/templates/default/dist/icons/stacks/java.svg +0 -1
- package/templates/default/dist/icons/stacks/javascript.svg +0 -1
- package/templates/default/dist/icons/stacks/kotlin.svg +0 -1
- package/templates/default/dist/icons/stacks/mongodb.svg +0 -1
- package/templates/default/dist/icons/stacks/nodejs.svg +0 -1
- package/templates/default/dist/icons/stacks/python.svg +0 -1
- package/templates/default/dist/icons/stacks/react-native.svg +0 -1
- package/templates/default/dist/icons/stacks/react.svg +0 -1
- package/templates/default/dist/icons/stacks/rust.svg +0 -1
- package/templates/default/dist/icons/stacks/solidity.svg +0 -1
- package/templates/default/dist/icons/stacks/swift.svg +0 -1
- package/templates/default/dist/icons/stacks/tensorflow.svg +0 -1
- package/templates/default/dist/icons/stacks/typescript.svg +0 -1
- package/templates/default/dist/index.html +0 -55
- package/templates/default/dist/llms-full.txt +0 -14
- package/templates/default/dist/llms.txt +0 -11
- package/templates/default/dist/og-image.svg +0 -48
- package/templates/default/dist/projects/index.html +0 -61
- package/templates/default/dist/robots.txt +0 -53
- package/templates/default/dist/sitemap.xml +0 -21
- package/templates/default/dist/submit/index.html +0 -69
- package/templates/default/node_modules/.bin/astro +0 -21
- package/templates/default/node_modules/.bin/astro-check +0 -21
- package/templates/default/node_modules/.bin/grove +0 -21
- package/templates/default/node_modules/.bin/jiti +0 -21
- package/templates/default/node_modules/.bin/prettier +0 -21
- package/templates/default/node_modules/.bin/rollup +0 -21
- package/templates/default/node_modules/.bin/tsc +0 -21
- package/templates/default/node_modules/.bin/tsserver +0 -21
- package/templates/default/node_modules/.bin/tsx +0 -21
- package/templates/default/node_modules/.bin/vite +0 -21
- package/templates/default/node_modules/.bin/yaml +0 -21
- package/templates/default/node_modules/.vite/deps/_metadata.json +0 -79
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +0 -433
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js +0 -5
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +0 -5
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +0 -9
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +0 -241
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +0 -1592
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +0 -30
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +0 -186
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +0 -48
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +0 -6779
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +0 -33
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +0 -88
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +0 -72
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +0 -3757
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +0 -54
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/package.json +0 -3
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +0 -213
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +0 -525
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +0 -1020
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +0 -7
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +0 -147
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +0 -7
- package/templates/default/src/components/layout/BaseLayout.astro +0 -47
- package/templates/default/src/components/layout/Container.astro +0 -18
- package/templates/default/src/components/layout/Footer.astro +0 -27
- package/templates/default/src/components/layout/Header.astro +0 -43
- package/templates/default/src/lib/markdown.ts +0 -171
- package/templates/default/src/pages/projects/index.astro +0 -193
|
@@ -1,99 +1,182 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* About page —
|
|
4
|
-
* methodology section rendered from `content/pages/methodology.md`.
|
|
3
|
+
* About page — generic.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* Mirrors the openapps section structure: breadcrumb, header with
|
|
6
|
+
* two-line intro, then a stack of cards (Why, Value, Data, Generic)
|
|
7
|
+
* followed by a CTA row. The copy is generic — references "directory",
|
|
8
|
+
* "record", "framework" — so it works for any blueprint.
|
|
9
|
+
*
|
|
10
|
+
* Side-by-side comparison with the older "Why" copy:
|
|
11
|
+
* - Old: "A directory of real open-source apps. Started from
|
|
12
|
+
* the Open Source Flutter Apps collection…"
|
|
13
|
+
* - New: "A growing community knowledge site. A curated, health-
|
|
14
|
+
* aware developer directory…"
|
|
15
|
+
*
|
|
16
|
+
* The card utility classes (`grove-card`, `grove-btn-*`) match the
|
|
17
|
+
* rest of the framework, and the data fields it references
|
|
18
|
+
* (`siteConfig.tagline`, `siteConfig.repoUrl`) all flow from the
|
|
19
|
+
* site config — no hardcoded brand or copy.
|
|
11
20
|
*/
|
|
12
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
13
|
-
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { dirname, resolve } from "node:path";
|
|
15
|
-
|
|
16
|
-
import siteConfig from "../../data/generated/site-config.json";
|
|
17
21
|
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
18
22
|
import Container from "@grove-dev/astro/layouts/Container.astro";
|
|
19
|
-
import
|
|
23
|
+
import siteConfig from "../../data/generated/site-config.json";
|
|
24
|
+
import { indexSlug, itemLabel } from "../data/records";
|
|
20
25
|
|
|
26
|
+
const githubUrl = siteConfig.repoUrl;
|
|
21
27
|
const title = `About — ${siteConfig.name}`;
|
|
22
|
-
const description =
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const methodologyPath = [
|
|
27
|
-
resolve(here, "..", "..", "..", "content", "pages", "methodology.md"),
|
|
28
|
-
resolve(here, "..", "..", "..", "..", "content", "pages", "methodology.md"),
|
|
29
|
-
resolve(process.cwd(), "content", "pages", "methodology.md"),
|
|
30
|
-
].find((p) => existsSync(p));
|
|
31
|
-
|
|
32
|
-
let methodologyText: string | null = null;
|
|
33
|
-
if (methodologyPath) {
|
|
34
|
-
try {
|
|
35
|
-
methodologyText = readFileSync(methodologyPath, "utf8");
|
|
36
|
-
} catch {
|
|
37
|
-
methodologyText = null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
28
|
+
const description = `${siteConfig.tagline} Built with Grove.`;
|
|
29
|
+
const slug = indexSlug();
|
|
30
|
+
const item = itemLabel();
|
|
40
31
|
---
|
|
41
32
|
|
|
42
33
|
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
43
|
-
<Container>
|
|
34
|
+
<Container size="narrow">
|
|
44
35
|
<article class="py-12 sm:py-16">
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
</h1>
|
|
51
|
-
<p class="mt-4 max-w-[62ch] text-base text-ink-700 dark:text-ink-200 m-0">
|
|
52
|
-
{siteConfig.name} is a community knowledge site built with{" "}
|
|
53
|
-
<a
|
|
54
|
-
href="https://github.com/tortuvshin/grove"
|
|
55
|
-
target="_blank"
|
|
56
|
-
rel="noopener noreferrer"
|
|
57
|
-
class="text-ink-900 underline hover:text-ink-700 dark:text-ink-100 dark:hover:text-white"
|
|
58
|
-
>
|
|
59
|
-
Grove
|
|
60
|
-
</a>{" "}
|
|
61
|
-
— an open-source framework for growing useful structured knowledge.
|
|
62
|
-
</p>
|
|
63
|
-
</article>
|
|
64
|
-
</Container>
|
|
36
|
+
<nav class="text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
37
|
+
<a href="/" class="hover:text-ink-900 dark:hover:text-ink-100">Home</a>
|
|
38
|
+
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
39
|
+
<span class="text-ink-700 dark:text-ink-300">About</span>
|
|
40
|
+
</nav>
|
|
65
41
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
<header class="mt-5">
|
|
43
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
44
|
+
About
|
|
45
|
+
</span>
|
|
46
|
+
<h1 class="mt-1 m-0 text-[1.75rem] sm:text-[2.25rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100">
|
|
47
|
+
A growing community knowledge site.
|
|
48
|
+
</h1>
|
|
49
|
+
<p class="mt-4 m-0 text-[1rem] sm:text-[1.0625rem] leading-relaxed text-ink-700 dark:text-ink-200 max-w-[62ch]">
|
|
50
|
+
{siteConfig.name} is a curated, health-aware developer directory —
|
|
51
|
+
built so you can find, compare, run, study, and contribute to
|
|
52
|
+
open-source {item}s.
|
|
53
|
+
</p>
|
|
54
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 max-w-[62ch]">
|
|
55
|
+
A growing list, forked from the original. Same shape, same
|
|
56
|
+
spirit, fresh data.
|
|
57
|
+
</p>
|
|
58
|
+
</header>
|
|
70
59
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
class="mt-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class="mt-6 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"
|
|
90
|
-
><code>{methodologyText}</code></pre>
|
|
91
|
-
) : (
|
|
92
|
-
<p class="mt-6 max-w-[62ch] text-sm text-ink-500 dark:text-ink-400 m-0">
|
|
93
|
-
The methodology file is not yet present in this scaffold.
|
|
94
|
-
See <code class="font-mono">content/pages/methodology.md</code>.
|
|
60
|
+
<section class="grove-card mt-8 p-5 sm:p-6">
|
|
61
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
62
|
+
Why
|
|
63
|
+
</span>
|
|
64
|
+
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
65
|
+
A curated starting point
|
|
66
|
+
</h2>
|
|
67
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
68
|
+
GitHub search and README lists optimize for the repository
|
|
69
|
+
name and description. They rarely tell you whether a project
|
|
70
|
+
is a real {item} or a demo, which stack it uses beyond the
|
|
71
|
+
headline, how active it really is, or whether it has a clear
|
|
72
|
+
license.
|
|
73
|
+
</p>
|
|
74
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
75
|
+
{siteConfig.name} turns that flat list into a structured,
|
|
76
|
+
health-aware index. Each entry carries the metadata a developer
|
|
77
|
+
actually needs to decide whether to open the repo.
|
|
95
78
|
</p>
|
|
96
|
-
|
|
97
|
-
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section class="grove-card mt-4 p-5 sm:p-6">
|
|
82
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
83
|
+
Why real {item}s
|
|
84
|
+
</span>
|
|
85
|
+
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
86
|
+
Real, runnable source
|
|
87
|
+
</h2>
|
|
88
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
89
|
+
Every {item} links back to its source. Skim, fork, learn — no
|
|
90
|
+
paywalls, no broken download links.
|
|
91
|
+
</p>
|
|
92
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
93
|
+
The directory is hand-curated — no SEO spam, no affiliate
|
|
94
|
+
farms, no broken links.
|
|
95
|
+
</p>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<section class="grove-card mt-4 p-5 sm:p-6">
|
|
99
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
100
|
+
What you get
|
|
101
|
+
</span>
|
|
102
|
+
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
103
|
+
What every page gives you
|
|
104
|
+
</h2>
|
|
105
|
+
<ul class="m-0 mt-3 list-none space-y-1.5 p-0 text-[0.9375rem] text-ink-700 dark:text-ink-200">
|
|
106
|
+
<li class="flex items-baseline gap-2">
|
|
107
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
108
|
+
<span>Stack, category, and architecture at a glance</span>
|
|
109
|
+
</li>
|
|
110
|
+
<li class="flex items-baseline gap-2">
|
|
111
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
112
|
+
<span>Supported platforms and license</span>
|
|
113
|
+
</li>
|
|
114
|
+
<li class="flex items-baseline gap-2">
|
|
115
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
116
|
+
<span>Activity and status — last commit, monthly history, health signals</span>
|
|
117
|
+
</li>
|
|
118
|
+
<li class="flex items-baseline gap-2">
|
|
119
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
120
|
+
<span>License, stars, and curation tier</span>
|
|
121
|
+
</li>
|
|
122
|
+
<li class="flex items-baseline gap-2">
|
|
123
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
124
|
+
<span>Curated notes: best for, why listed, caveats</span>
|
|
125
|
+
</li>
|
|
126
|
+
<li class="flex items-baseline gap-2">
|
|
127
|
+
<span aria-hidden="true" class="text-ink-400 dark:text-ink-500">—</span>
|
|
128
|
+
<span>Filters that respect you — search by stack, platform, license, or status</span>
|
|
129
|
+
</li>
|
|
130
|
+
</ul>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<section class="grove-card mt-4 p-5 sm:p-6">
|
|
134
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
135
|
+
Generic by design
|
|
136
|
+
</span>
|
|
137
|
+
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
138
|
+
Generic by design
|
|
139
|
+
</h2>
|
|
140
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
141
|
+
The framework supports three blueprints: project-directory,
|
|
142
|
+
resource-hub, and ecosystem-map. Each can be configured
|
|
143
|
+
without touching the components — pages, schema, and
|
|
144
|
+
routes all derive from <code class="font-mono">grove.config.ts</code>.
|
|
145
|
+
</p>
|
|
146
|
+
</section>
|
|
147
|
+
|
|
148
|
+
<section class="grove-card mt-4 p-5 sm:p-6">
|
|
149
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
150
|
+
Free to fork, free to extend
|
|
151
|
+
</span>
|
|
152
|
+
<h2 class="mt-1 m-0 text-[1.125rem] sm:text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
153
|
+
Free to fork, free to extend
|
|
154
|
+
</h2>
|
|
155
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
156
|
+
The full data lives in this repository as plain YAML. The
|
|
157
|
+
static output is plain HTML. No database, no JavaScript
|
|
158
|
+
framework, no CDN with sticky sessions.
|
|
159
|
+
</p>
|
|
160
|
+
<p class="mt-3 m-0 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">
|
|
161
|
+
Anyone can suggest a new {item}, fix a typo, or improve the
|
|
162
|
+
metadata through a pull request.
|
|
163
|
+
</p>
|
|
164
|
+
</section>
|
|
165
|
+
|
|
166
|
+
<section class="mt-8 flex flex-wrap items-center gap-3">
|
|
167
|
+
<a href={`/${slug}`} class="grove-btn grove-btn-primary">Browse the directory</a>
|
|
168
|
+
{githubUrl && (
|
|
169
|
+
<a
|
|
170
|
+
href={githubUrl}
|
|
171
|
+
target="_blank"
|
|
172
|
+
rel="noopener noreferrer"
|
|
173
|
+
class="grove-btn grove-btn-outline"
|
|
174
|
+
>
|
|
175
|
+
View on GitHub
|
|
176
|
+
</a>
|
|
177
|
+
)}
|
|
178
|
+
<a href="/submit" class="grove-btn grove-btn-ghost">Submit a {item} →</a>
|
|
179
|
+
</section>
|
|
180
|
+
</article>
|
|
98
181
|
</Container>
|
|
99
182
|
</BaseLayout>
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* /apps/<slug> — alternate detail URL.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `/[slug]/[itemSlug].astro` (the blueprint-aware dynamic
|
|
6
|
+
* detail page) at a static URL. For non-project blueprints
|
|
7
|
+
* (resource-hub, ecosystem-map), redirect to the matching dynamic
|
|
8
|
+
* route.
|
|
9
|
+
*/
|
|
10
|
+
import { indexSlug, fullRecords, projectBySlug, recordBySlug } from "../../data/records";
|
|
11
|
+
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
12
|
+
import Icon from "@grove-dev/astro/components/Icon.astro";
|
|
13
|
+
import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
|
|
14
|
+
import siteConfig from "../../../data/generated/site-config.json";
|
|
15
|
+
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, formatRelative, formatStars, statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
|
|
16
|
+
|
|
17
|
+
export async function getStaticPaths() {
|
|
18
|
+
return fullRecords.map((r) => ({ params: { itemSlug: r.slug } }));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const slug = indexSlug();
|
|
22
|
+
const itemSlug = Astro.params.itemSlug ?? "";
|
|
23
|
+
|
|
24
|
+
if (slug !== "projects") {
|
|
25
|
+
return Astro.redirect(`/${slug}/${itemSlug}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const record = recordBySlug(itemSlug);
|
|
29
|
+
if (!record) {
|
|
30
|
+
return Astro.redirect("/apps");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const project = projectBySlug(itemSlug);
|
|
34
|
+
const isProject = record.kind === "project";
|
|
35
|
+
const name = record.kind === "resource" ? record.title : record.name;
|
|
36
|
+
const repoUrl = project?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
|
|
37
|
+
const homepageUrl = record.links?.website ?? "";
|
|
38
|
+
const logoUrl = project?.logoUrl;
|
|
39
|
+
const stacks = project?.stacks ?? (project?.stack ? [project.stack] : []);
|
|
40
|
+
const platforms = project?.platforms ?? [];
|
|
41
|
+
const github = project?.github?.repository;
|
|
42
|
+
const stars = github?.stargazers_count ?? 0;
|
|
43
|
+
const starsLabel = formatStars(stars);
|
|
44
|
+
const pushedAt = github?.pushed_at ?? null;
|
|
45
|
+
const pushedLabel = formatRelative(pushedAt);
|
|
46
|
+
const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
|
|
47
|
+
const avatarSrc = logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
|
|
48
|
+
const initials = (name || "").replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase();
|
|
49
|
+
const bestFor = project?.bestFor ?? [];
|
|
50
|
+
const whyListed = project?.whyListed ?? [];
|
|
51
|
+
const caveats = project?.caveats ?? [];
|
|
52
|
+
const distribution = project?.distribution?.channels ?? [];
|
|
53
|
+
const scores = project?.scores ?? null;
|
|
54
|
+
const curationLabels = record.curation?.labels ?? [];
|
|
55
|
+
const tags = record.tags ?? [];
|
|
56
|
+
const healthStatus = project?.health?.status;
|
|
57
|
+
const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
|
|
58
|
+
|
|
59
|
+
const title = `${name} — ${siteConfig.name}`;
|
|
60
|
+
const description = record.description ?? `${name} on ${siteConfig.name}`;
|
|
61
|
+
|
|
62
|
+
const jsonLd: Record<string, unknown> | null = project
|
|
63
|
+
? {
|
|
64
|
+
"@context": "https://schema.org",
|
|
65
|
+
"@type": "SoftwareSourceCode",
|
|
66
|
+
name,
|
|
67
|
+
description: record.description ?? undefined,
|
|
68
|
+
url: repoUrl || homepageUrl || undefined,
|
|
69
|
+
codeRepository: repoUrl || undefined,
|
|
70
|
+
programmingLanguage: github?.language ?? undefined,
|
|
71
|
+
license: github?.license?.spdx_id ?? undefined,
|
|
72
|
+
author: ownerRepo
|
|
73
|
+
? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
|
|
74
|
+
: undefined,
|
|
75
|
+
}
|
|
76
|
+
: null;
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<BaseLayout
|
|
80
|
+
title={title}
|
|
81
|
+
description={description}
|
|
82
|
+
site={siteConfig}
|
|
83
|
+
type="article"
|
|
84
|
+
jsonLd={jsonLd ?? undefined}
|
|
85
|
+
submitHref="/submit"
|
|
86
|
+
submitLabel={`Submit ${name}`}
|
|
87
|
+
>
|
|
88
|
+
<article class="grove-section">
|
|
89
|
+
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
90
|
+
<nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
91
|
+
<a href="/apps" class="hover:text-ink-900 dark:hover:text-ink-100">Apps</a>
|
|
92
|
+
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
93
|
+
<span class="text-ink-700 dark:text-ink-300">{name}</span>
|
|
94
|
+
</nav>
|
|
95
|
+
|
|
96
|
+
<header class="grove-card flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
|
|
97
|
+
<div class="flex-shrink-0">
|
|
98
|
+
{avatarSrc ? (
|
|
99
|
+
<img
|
|
100
|
+
src={avatarSrc}
|
|
101
|
+
alt=""
|
|
102
|
+
width="64"
|
|
103
|
+
height="64"
|
|
104
|
+
loading="lazy"
|
|
105
|
+
decoding="async"
|
|
106
|
+
class="h-16 w-16 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950"
|
|
107
|
+
/>
|
|
108
|
+
) : (
|
|
109
|
+
<span
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
class="inline-flex h-16 w-16 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-2xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
|
|
112
|
+
>
|
|
113
|
+
{initials || "·"}
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="min-w-0 flex-1">
|
|
119
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
120
|
+
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
121
|
+
{record.kind}
|
|
122
|
+
</span>
|
|
123
|
+
{record.category && (
|
|
124
|
+
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
125
|
+
{record.category}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
{curationLabels.map((l) => (
|
|
129
|
+
<span class="grove-badge" title={`Curated: ${labelDisplay(l) ?? l}`}>
|
|
130
|
+
{labelDisplay(l) ?? prettySlug(l)}
|
|
131
|
+
</span>
|
|
132
|
+
))}
|
|
133
|
+
{healthLabel && (
|
|
134
|
+
<span class="grove-badge grove-badge-stale">{healthLabel}</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<h1 class="m-0 mt-2 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
139
|
+
{name}
|
|
140
|
+
</h1>
|
|
141
|
+
|
|
142
|
+
{ownerRepo && (
|
|
143
|
+
<p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
|
|
144
|
+
<a
|
|
145
|
+
href={`https://github.com/${ownerRepo.owner}`}
|
|
146
|
+
target="_blank"
|
|
147
|
+
rel="noopener noreferrer"
|
|
148
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
149
|
+
>
|
|
150
|
+
@{ownerRepo.owner}
|
|
151
|
+
</a>
|
|
152
|
+
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
153
|
+
<a
|
|
154
|
+
href={repoUrl}
|
|
155
|
+
target="_blank"
|
|
156
|
+
rel="noopener noreferrer"
|
|
157
|
+
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
158
|
+
>
|
|
159
|
+
{ownerRepo.repo}
|
|
160
|
+
</a>
|
|
161
|
+
</p>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{record.description && (
|
|
165
|
+
<p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
166
|
+
{record.description}
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<div class="mt-5 flex flex-wrap gap-2">
|
|
171
|
+
{repoUrl && (
|
|
172
|
+
<a href={repoUrl} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-primary">
|
|
173
|
+
View on GitHub →
|
|
174
|
+
</a>
|
|
175
|
+
)}
|
|
176
|
+
{homepageUrl && (
|
|
177
|
+
<a href={homepageUrl} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-outline">
|
|
178
|
+
Homepage
|
|
179
|
+
</a>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</header>
|
|
184
|
+
|
|
185
|
+
{project && (
|
|
186
|
+
<section class="mt-8 grove-card p-6" aria-labelledby="scores-heading">
|
|
187
|
+
<h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
188
|
+
Scores
|
|
189
|
+
</h2>
|
|
190
|
+
<div class="mt-4">
|
|
191
|
+
<ScoreBars scores={scores} />
|
|
192
|
+
</div>
|
|
193
|
+
</section>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* BestFor / WhyListed / Caveats grid */}
|
|
197
|
+
{isProject && (bestFor.length > 0 || whyListed.length > 0 || caveats.length > 0) && (
|
|
198
|
+
<section class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3" aria-label="Curation notes">
|
|
199
|
+
{bestFor.length > 0 && (
|
|
200
|
+
<div class="grove-card p-5">
|
|
201
|
+
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Best for</h3>
|
|
202
|
+
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
203
|
+
{bestFor.map((b) => <li class="text-sm text-ink-700 dark:text-ink-200">— {b}</li>)}
|
|
204
|
+
</ul>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
{whyListed.length > 0 && (
|
|
208
|
+
<div class="grove-card p-5">
|
|
209
|
+
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Why listed</h3>
|
|
210
|
+
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
211
|
+
{whyListed.map((w) => <li class="text-sm text-ink-700 dark:text-ink-200">— {w}</li>)}
|
|
212
|
+
</ul>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
{caveats.length > 0 && (
|
|
216
|
+
<div class="grove-card p-5">
|
|
217
|
+
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Caveats</h3>
|
|
218
|
+
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
219
|
+
{caveats.map((c) => <li class="text-sm text-ink-700 dark:text-ink-200">— {c}</li>)}
|
|
220
|
+
</ul>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</section>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
{/* Stack + Platform chips */}
|
|
227
|
+
{isProject && (stacks.length > 0 || platforms.length > 0) && (
|
|
228
|
+
<section class="mt-8" aria-labelledby="stacks-heading">
|
|
229
|
+
<h2 id="stacks-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
230
|
+
Stack & platforms
|
|
231
|
+
</h2>
|
|
232
|
+
<div class="mt-4 flex flex-col gap-3">
|
|
233
|
+
{stacks.length > 0 && (
|
|
234
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
235
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Stack</span>
|
|
236
|
+
{stacks.map((s) => (
|
|
237
|
+
<span class="grove-badge inline-flex items-center gap-1.5">
|
|
238
|
+
<Icon name={s} category="stack" size={14} />
|
|
239
|
+
{s}
|
|
240
|
+
</span>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
{platforms.length > 0 && (
|
|
245
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
246
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Platform</span>
|
|
247
|
+
{platforms.map((p) => (
|
|
248
|
+
<span class="grove-badge inline-flex items-center gap-1.5">
|
|
249
|
+
<Icon name={p} category="platform" size={14} />
|
|
250
|
+
{p}
|
|
251
|
+
</span>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
</section>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{/* Tags */}
|
|
260
|
+
{tags.length > 0 && (
|
|
261
|
+
<section class="mt-8" aria-labelledby="tags-heading">
|
|
262
|
+
<h2 id="tags-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
263
|
+
Tags
|
|
264
|
+
</h2>
|
|
265
|
+
<ul class="mt-4 m-0 flex list-none flex-wrap gap-1.5 p-0">
|
|
266
|
+
{tags.map((t) => (
|
|
267
|
+
<li>
|
|
268
|
+
<a href={`/apps?q=${encodeURIComponent(t)}`} class="grove-badge inline-flex items-center gap-1.5">{t}</a>
|
|
269
|
+
</li>
|
|
270
|
+
))}
|
|
271
|
+
</ul>
|
|
272
|
+
</section>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Distribution channels */}
|
|
276
|
+
{distribution.length > 0 && (
|
|
277
|
+
<section class="mt-8" aria-labelledby="dist-heading">
|
|
278
|
+
<h2 id="dist-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
279
|
+
Distribution channels
|
|
280
|
+
</h2>
|
|
281
|
+
<ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
|
|
282
|
+
{distribution.map((c) => (
|
|
283
|
+
<li class="grove-card flex flex-wrap items-baseline justify-between gap-2 p-3">
|
|
284
|
+
<div class="flex flex-col">
|
|
285
|
+
<span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
|
|
286
|
+
{c.label ?? c.type}
|
|
287
|
+
{c.verified && <span class="ml-2 grove-badge grove-badge-new">Verified</span>}
|
|
288
|
+
</span>
|
|
289
|
+
{c.notes && <span class="text-xs text-ink-500 dark:text-ink-400">{c.notes}</span>}
|
|
290
|
+
</div>
|
|
291
|
+
<a href={c.url} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-outline">
|
|
292
|
+
{c.platform ?? c.type} →
|
|
293
|
+
</a>
|
|
294
|
+
</li>
|
|
295
|
+
))}
|
|
296
|
+
</ul>
|
|
297
|
+
</section>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
</article>
|
|
301
|
+
</BaseLayout>
|