@grove-dev/astro 0.2.10 → 0.2.14
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/README.md +26 -18
- 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 +12 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +13 -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 +44 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +192 -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/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/IndexRow.astro +75 -0
- package/src/components/ItemCard.astro +266 -248
- package/src/components/Pagination.astro +63 -0
- package/src/components/RecordSection.astro +70 -34
- 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 +17 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +218 -291
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +209 -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 +138 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[recordSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +477 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[recordSlug].astro +33 -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/src/components/AppsIndexRow.astro +0 -204
- package/src/components/AppsPagination.astro +0 -126
- 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
package/src/lib/lenses.ts
CHANGED
|
@@ -1,162 +1,123 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Curated lenses for the list
|
|
2
|
+
* Curated lenses for the list page.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two flavors:
|
|
5
|
+
* - "label-based": maps to a single `labels` value (e.g. new / hot / mature).
|
|
6
|
+
* No extra curation needed — every item already has a label.
|
|
7
|
+
* - "curator-assigned": maps to a `lenses` value on the item. The 5 anchor
|
|
8
|
+
* items get these populated. Other items will simply not match.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - "new" — fresh entries: pushed within 7d OR curated "new" label
|
|
11
|
-
* - "mature" — long-running projects: curated tier AND pushed > 365d ago
|
|
12
|
-
* - "featured" — curated "featured" label
|
|
13
|
-
* - "needs-review" — cleanup candidate (stale / archived / etc.)
|
|
10
|
+
* The lens id is what shows up in the URL as `?lens=...`. For
|
|
11
|
+
* label-based lenses, the lens id is the same as the label.
|
|
14
12
|
*
|
|
15
|
-
* The
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* Keep this list in sync with `lib/search.ts` and any UI tabs.
|
|
13
|
+
* The 6 lenses in `PRIMARY_LENSES` are rendered as top-row tabs. The rest
|
|
14
|
+
* remain in the union type for URL deep-linking and future use, but are
|
|
15
|
+
* not shown in the UI by default.
|
|
19
16
|
*/
|
|
20
17
|
|
|
21
|
-
import type {
|
|
22
|
-
HealthStatus,
|
|
23
|
-
ProjectRecord,
|
|
24
|
-
} from "@grove-dev/core";
|
|
25
|
-
|
|
26
18
|
export type LensId =
|
|
19
|
+
// Label-based (tabbed)
|
|
27
20
|
| "all"
|
|
28
|
-
| "hot"
|
|
29
21
|
| "new"
|
|
22
|
+
| "hot"
|
|
30
23
|
| "mature"
|
|
31
|
-
|
|
32
|
-
| "
|
|
24
|
+
// Curator-assigned (tabbed)
|
|
25
|
+
| "good-to-learn"
|
|
26
|
+
| "production-like"
|
|
27
|
+
// Available in URL state, but not tabbed
|
|
28
|
+
| "beginner-friendly"
|
|
29
|
+
| "contribution-ready"
|
|
30
|
+
| "launches"
|
|
31
|
+
// Status-based (available in URL state, not tabbed)
|
|
32
|
+
| "actively-developed"
|
|
33
|
+
| "needs-maintainer";
|
|
33
34
|
|
|
34
35
|
export interface LensDef {
|
|
35
36
|
id: LensId;
|
|
36
37
|
label: string;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* same record the list page already holds — so consumers don't
|
|
42
|
-
* need to do a second fetch.
|
|
43
|
-
*/
|
|
44
|
-
match: (record: AppLike) => boolean;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const DAY_MS = 86_400_000;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The shape that lens matching actually reads. Decoupled from
|
|
51
|
-
* `ProjectRecord` so the search lib can also pass the index payload
|
|
52
|
-
* (which omits a few heavy fields) and tests can pass minimal
|
|
53
|
-
* fixtures.
|
|
54
|
-
*/
|
|
55
|
-
export interface AppLike {
|
|
56
|
-
curation?: { labels?: string[] };
|
|
57
|
-
health?: {
|
|
58
|
-
status?: HealthStatus;
|
|
59
|
-
tier?: "curated" | "listed" | "experimental" | "hidden";
|
|
60
|
-
cleanupCandidate?: boolean;
|
|
61
|
-
};
|
|
62
|
-
github?: {
|
|
63
|
-
stars?: number;
|
|
64
|
-
pushedAt?: string | null;
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function daysSince(iso: string | null | undefined): number {
|
|
69
|
-
if (!iso) return Infinity;
|
|
70
|
-
const d = new Date(iso);
|
|
71
|
-
if (Number.isNaN(d.valueOf())) return Infinity;
|
|
72
|
-
return (Date.now() - d.valueOf()) / DAY_MS;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function hasLabel(record: AppLike, label: string): boolean {
|
|
76
|
-
return record.curation?.labels?.includes(label) ?? false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const CLEANUP_STATUSES: HealthStatus[] = [
|
|
80
|
-
"stale",
|
|
81
|
-
"inactive",
|
|
82
|
-
"archived",
|
|
83
|
-
"unavailable",
|
|
84
|
-
"needs_review",
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
function isCleanupCandidate(record: AppLike): boolean {
|
|
88
|
-
if (record.health?.cleanupCandidate) return true;
|
|
89
|
-
const status = record.health?.status;
|
|
90
|
-
if (!status) return false;
|
|
91
|
-
return CLEANUP_STATUSES.includes(status);
|
|
38
|
+
/** Short blurb shown when the lens is active. */
|
|
39
|
+
description?: string;
|
|
40
|
+
/** What URL state this lens implies. Empty means no filter. */
|
|
41
|
+
toParams: () => Record<string, string | string[]>;
|
|
92
42
|
}
|
|
93
43
|
|
|
94
44
|
export const LENSES: LensDef[] = [
|
|
45
|
+
{ id: "all", label: "All items", description: "Every item in the directory", toParams: () => ({}) },
|
|
95
46
|
{
|
|
96
|
-
id: "
|
|
97
|
-
label: "
|
|
98
|
-
description: "
|
|
99
|
-
|
|
47
|
+
id: "new",
|
|
48
|
+
label: "Recently added",
|
|
49
|
+
description: "Apps recently added to the directory",
|
|
50
|
+
toParams: () => ({ label: "new" }),
|
|
100
51
|
},
|
|
101
52
|
{
|
|
102
53
|
id: "hot",
|
|
103
54
|
label: "Trending",
|
|
104
|
-
description:
|
|
105
|
-
|
|
106
|
-
match: (r) => {
|
|
107
|
-
const stars = r.github?.stars ?? 0;
|
|
108
|
-
const pushed = daysSince(r.github?.pushedAt);
|
|
109
|
-
return stars >= 1000 || pushed <= 30;
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
id: "new",
|
|
114
|
-
label: "Recently added",
|
|
115
|
-
description:
|
|
116
|
-
"Fresh entries — pushed within 7 days or curated with the “new” label.",
|
|
117
|
-
match: (r) => {
|
|
118
|
-
const pushed = daysSince(r.github?.pushedAt);
|
|
119
|
-
return pushed <= 7 || hasLabel(r, "new");
|
|
120
|
-
},
|
|
55
|
+
description: "Apps with recent activity and attention",
|
|
56
|
+
toParams: () => ({ label: "hot" }),
|
|
121
57
|
},
|
|
122
58
|
{
|
|
123
59
|
id: "mature",
|
|
124
60
|
label: "Established",
|
|
125
|
-
description:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
61
|
+
description: "Long-running projects with stable history",
|
|
62
|
+
toParams: () => ({ label: "mature" }),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "production-like",
|
|
66
|
+
label: "Production-like",
|
|
67
|
+
description: "Real items, not toy projects",
|
|
68
|
+
toParams: () => ({ lens: "production-like" }),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "good-to-learn",
|
|
72
|
+
label: "Good to learn",
|
|
73
|
+
description: "Readable codebases with useful patterns",
|
|
74
|
+
toParams: () => ({ lens: "good-to-learn" }),
|
|
75
|
+
},
|
|
76
|
+
// Secondary (URL-only, not rendered as tabs)
|
|
77
|
+
{
|
|
78
|
+
id: "beginner-friendly",
|
|
79
|
+
label: "Beginner friendly",
|
|
80
|
+
description: "Smaller, readable, easier to understand",
|
|
81
|
+
toParams: () => ({ lens: "beginner-friendly" }),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "contribution-ready",
|
|
85
|
+
label: "Contribution ready",
|
|
86
|
+
description: "Clear issues, active maintainers, license, contribution docs",
|
|
87
|
+
toParams: () => ({ lens: "contribution-ready" }),
|
|
132
88
|
},
|
|
133
89
|
{
|
|
134
|
-
id: "
|
|
135
|
-
label: "
|
|
136
|
-
description: "
|
|
137
|
-
|
|
90
|
+
id: "launches",
|
|
91
|
+
label: "Launches",
|
|
92
|
+
description: "Recently launched OSS items seeking feedback",
|
|
93
|
+
toParams: () => ({ lens: "launches" }),
|
|
138
94
|
},
|
|
139
95
|
{
|
|
140
|
-
id: "
|
|
141
|
-
label: "
|
|
142
|
-
description: "
|
|
143
|
-
|
|
96
|
+
id: "actively-developed",
|
|
97
|
+
label: "Active",
|
|
98
|
+
description: "Apps with recent commits, releases, and issue activity",
|
|
99
|
+
toParams: () => ({ status: "active" }),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "needs-maintainer",
|
|
103
|
+
label: "Needs maintainer",
|
|
104
|
+
description: "Useful items that need help",
|
|
105
|
+
toParams: () => ({ status: "stale,quiet" }),
|
|
144
106
|
},
|
|
145
107
|
];
|
|
146
108
|
|
|
147
109
|
/**
|
|
148
|
-
* The lenses
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* in the primary row.
|
|
110
|
+
* The 6 lenses shown as top-row tabs on /items. Order matters — left to right.
|
|
111
|
+
* Keep this list aligned with what the design calls for: All, signal lenses,
|
|
112
|
+
* and the two curator-assigned lenses that have any matches.
|
|
152
113
|
*/
|
|
153
114
|
export const PRIMARY_LENSES: LensId[] = [
|
|
154
115
|
"all",
|
|
155
|
-
"hot",
|
|
156
116
|
"new",
|
|
117
|
+
"hot",
|
|
157
118
|
"mature",
|
|
158
|
-
"
|
|
159
|
-
"
|
|
119
|
+
"production-like",
|
|
120
|
+
"good-to-learn",
|
|
160
121
|
];
|
|
161
122
|
|
|
162
123
|
export function lensById(id: string | null | undefined): LensDef | undefined {
|
|
@@ -164,78 +125,96 @@ export function lensById(id: string | null | undefined): LensDef | undefined {
|
|
|
164
125
|
return LENSES.find((l) => l.id === id);
|
|
165
126
|
}
|
|
166
127
|
|
|
167
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Check whether the current URL search params match a given lens's
|
|
130
|
+
* implied filter state. Used to highlight the active tab when a
|
|
131
|
+
* deep-link uses the underlying filter key (e.g. ?label=hot) rather
|
|
132
|
+
* than the abstract lens id (?lens=hot).
|
|
133
|
+
*/
|
|
134
|
+
export function isLensActive(lensId: LensId, sp: URLSearchParams): boolean {
|
|
135
|
+
if (lensId === "all") {
|
|
136
|
+
// "All" is active when no lens-shaped filter is present.
|
|
137
|
+
return !lensFromSearchParams(sp) && !sp.get("label") && !sp.get("status");
|
|
138
|
+
}
|
|
139
|
+
const def = lensById(lensId);
|
|
140
|
+
if (!def) return false;
|
|
141
|
+
const target = def.toParams();
|
|
142
|
+
for (const [k, v] of Object.entries(target)) {
|
|
143
|
+
if (Array.isArray(v)) {
|
|
144
|
+
if (sp.getAll(k).length !== v.length) return false;
|
|
145
|
+
for (const item of v) if (!sp.getAll(k).includes(item)) return false;
|
|
146
|
+
} else {
|
|
147
|
+
if (sp.get(k) !== v) return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function isPrimaryLens(id: string | null | undefined): boolean {
|
|
168
154
|
if (!id) return false;
|
|
169
155
|
return PRIMARY_LENSES.includes(id as LensId);
|
|
170
156
|
}
|
|
171
157
|
|
|
172
158
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* "absent" and "unknown" identically.
|
|
159
|
+
* Turn a lens's `toParams()` output into a URLSearchParams string
|
|
160
|
+
* (with the existing query preserved, then lens params merged on top).
|
|
176
161
|
*/
|
|
177
|
-
export function
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
162
|
+
export function lensToQuery(id: LensId | null | undefined, current: URLSearchParams): string {
|
|
163
|
+
if (!id || id === "all") return "";
|
|
164
|
+
const def = lensById(id);
|
|
165
|
+
if (!def) return "";
|
|
166
|
+
const next = new URLSearchParams(current);
|
|
167
|
+
for (const [k, v] of Object.entries(def.toParams())) {
|
|
168
|
+
if (Array.isArray(v)) {
|
|
169
|
+
next.delete(k);
|
|
170
|
+
for (const item of v) next.append(k, item);
|
|
171
|
+
} else {
|
|
172
|
+
next.set(k, v);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return next.toString();
|
|
184
176
|
}
|
|
185
177
|
|
|
186
178
|
/**
|
|
187
|
-
*
|
|
188
|
-
* filter shape. The "all" lens is the implicit "no filter" state —
|
|
189
|
-
* it's active only when no other lens-shaped params are present.
|
|
179
|
+
* Read a `?lens=...` value from URL search params.
|
|
190
180
|
*/
|
|
191
|
-
export function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return
|
|
181
|
+
export function lensFromSearchParams(sp: URLSearchParams): LensId | null {
|
|
182
|
+
const v = sp.get("lens");
|
|
183
|
+
if (!v) return null;
|
|
184
|
+
if (LENSES.some((l) => l.id === v)) return v as LensId;
|
|
185
|
+
return null;
|
|
196
186
|
}
|
|
197
187
|
|
|
198
188
|
/**
|
|
199
|
-
* Build a `href` for a lens tab.
|
|
200
|
-
* "view" params (label, page)
|
|
201
|
-
*
|
|
202
|
-
* preserved.
|
|
203
|
-
*
|
|
204
|
-
* `pathPrefix` is the URL root for the list page — typically
|
|
205
|
-
* `/projects` for the project-directory blueprint, `/resources` for
|
|
206
|
-
* resource-hub, `/entities` for ecosystem-map. Defaults to `/`.
|
|
189
|
+
* Build a `href` for a lens tab. Per the single-select-view rule,
|
|
190
|
+
* clicking a lens resets the "view" params (lens, label, status, page)
|
|
191
|
+
* and applies only the chosen lens's params. Stack/Platform/Category/
|
|
192
|
+
* License/q/sort/density filters are preserved.
|
|
207
193
|
*/
|
|
208
194
|
export function hrefForLens(
|
|
209
|
-
lensId: LensId,
|
|
210
|
-
|
|
211
|
-
pathPrefix
|
|
195
|
+
lensId: LensId | null | undefined,
|
|
196
|
+
current: URLSearchParams,
|
|
197
|
+
pathPrefix = "/items",
|
|
212
198
|
): string {
|
|
213
|
-
const sp = new URLSearchParams(
|
|
199
|
+
const sp = new URLSearchParams(current);
|
|
214
200
|
sp.delete("lens");
|
|
215
201
|
sp.delete("label");
|
|
202
|
+
sp.delete("status");
|
|
216
203
|
sp.delete("page");
|
|
217
204
|
if (lensId && lensId !== "all") {
|
|
218
|
-
|
|
205
|
+
const def = lensById(lensId);
|
|
206
|
+
if (def) {
|
|
207
|
+
for (const [k, v] of Object.entries(def.toParams())) {
|
|
208
|
+
if (Array.isArray(v)) {
|
|
209
|
+
sp.delete(k);
|
|
210
|
+
for (const item of v) sp.append(k, item);
|
|
211
|
+
} else {
|
|
212
|
+
sp.set(k, v);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
219
216
|
}
|
|
220
217
|
const qs = sp.toString();
|
|
221
|
-
const base = pathPrefix.replace(/\/$/, "");
|
|
222
|
-
return qs ? `${base}?${qs}` : base
|
|
218
|
+
const base = pathPrefix.replace(/\/$/, "") || "/";
|
|
219
|
+
return qs ? `${base}?${qs}` : base;
|
|
223
220
|
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Apply the lens filter to a record array. When `lensId` is
|
|
227
|
-
* `""` (no lens) or `"all"`, returns the input unchanged.
|
|
228
|
-
*
|
|
229
|
-
* Type-parameterized so the same helper works on `ProjectRecord[]`
|
|
230
|
-
* and on the lighter index payload that list pages ship.
|
|
231
|
-
*/
|
|
232
|
-
export function applyLens<T extends AppLike>(records: T[], lensId: LensId | ""): T[] {
|
|
233
|
-
if (!lensId || lensId === "all") return records;
|
|
234
|
-
const def = lensById(lensId);
|
|
235
|
-
if (!def) return records;
|
|
236
|
-
return records.filter((r) => def.match(r));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Re-export the type so consumers can write `import type { AppLike }`
|
|
240
|
-
// without depending on `@grove-dev/core` directly.
|
|
241
|
-
export type { ProjectRecord };
|
package/src/lib/repo.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse a GitHub repo URL into its owner and repo components.
|
|
3
|
-
*
|
|
4
|
-
* Returns `null` (not `{ owner: null, repo: null }`) if the URL
|
|
5
|
-
* doesn't match the github.com path shape — this lets callers
|
|
6
|
-
* distinguish "no URL" from "URL without owner" with a single
|
|
7
|
-
* null check.
|
|
3
|
+
* Returns nulls if the URL doesn't match github.com/{owner}/{repo}.
|
|
8
4
|
*
|
|
9
5
|
* Used in:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* - ItemCard avatar fallback (owner avatar via GitHub avatars
|
|
7
|
+
* API; component name retained for V1 published API stability)
|
|
8
|
+
* - App detail page (showing owner + repo name in breadcrumb / header)
|
|
9
|
+
* - Anywhere we need to display "@owner / repo" or link to the owner
|
|
14
10
|
*/
|
|
15
|
-
export function getOwnerAndRepoFromRepoUrl(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
export function getOwnerAndRepoFromRepoUrl(repoUrl: string): {
|
|
12
|
+
owner: string | null;
|
|
13
|
+
repo: string | null;
|
|
14
|
+
} {
|
|
15
|
+
if (!repoUrl || !repoUrl.includes("github.com/")) {
|
|
16
|
+
return { owner: null, repo: null };
|
|
17
|
+
}
|
|
19
18
|
const m = repoUrl.match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
20
|
-
if (!m) return null;
|
|
19
|
+
if (!m) return { owner: null, repo: null };
|
|
21
20
|
return {
|
|
22
21
|
owner: m[1],
|
|
23
22
|
repo: m[2].replace(/\.git$/, ""),
|
|
@@ -25,20 +24,10 @@ export function getOwnerAndRepoFromRepoUrl(
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
|
-
* Build a GitHub avatar URL for a given owner.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Returns a string (not nullable) so callers can plug the result
|
|
32
|
-
* straight into an `src` attribute. The avatar endpoint always
|
|
33
|
-
* returns a placeholder image for unknown owners, so the only
|
|
34
|
-
* reason to check `null` upstream is when you specifically want
|
|
35
|
-
* to render a different fallback (e.g. initials).
|
|
27
|
+
* Build a GitHub avatar URL for a given owner.
|
|
28
|
+
* Returns null if owner is null/empty.
|
|
36
29
|
*/
|
|
37
|
-
export function getOwnerAvatarUrl(owner: string, size = 80): string {
|
|
38
|
-
if (!owner)
|
|
39
|
-
// Defensive: callers may pass a possibly-empty string. Return the
|
|
40
|
-
// generic GitHub identicon so an `<img>` doesn't 404.
|
|
41
|
-
return `https://avatars.githubusercontent.com/identicon?v=4&s=${size}`;
|
|
42
|
-
}
|
|
30
|
+
export function getOwnerAvatarUrl(owner: string | null, size: number = 80): string | null {
|
|
31
|
+
if (!owner) return null;
|
|
43
32
|
return `https://avatars.githubusercontent.com/${owner}?v=4&s=${size}`;
|
|
44
33
|
}
|
package/src/lib/scores.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import type { Score } from "@grove-dev/core";
|
|
1
|
+
type AppScores = {
|
|
2
|
+
activity?: number;
|
|
3
|
+
maturity?: number;
|
|
4
|
+
learning?: number;
|
|
5
|
+
contribution?: number;
|
|
6
|
+
docs?: number;
|
|
7
|
+
overall?: number;
|
|
8
|
+
};
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Format a single score (0-100) into a 0-5 "tier" used by the UI:
|
|
@@ -17,8 +16,8 @@ import type { Score } from "@grove-dev/core";
|
|
|
17
16
|
* 60-79 → 3 (high)
|
|
18
17
|
* 80-100 → 4 (very high)
|
|
19
18
|
*
|
|
20
|
-
* The list page renders each score as a 4-cell bar; this gives
|
|
21
|
-
*
|
|
19
|
+
* The list page renders each score as a 4-cell bar; this gives the
|
|
20
|
+
* visual a stable shape regardless of the underlying number.
|
|
22
21
|
*/
|
|
23
22
|
export function scoreTier(n: number): 0 | 1 | 2 | 3 | 4 {
|
|
24
23
|
if (n < 20) return 0;
|
|
@@ -51,7 +50,7 @@ export function scoreLabel(n: number | undefined): string {
|
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/** Order of score dimensions, used by both list and detail. */
|
|
54
|
-
export const SCORE_DIMENSIONS: (keyof
|
|
53
|
+
export const SCORE_DIMENSIONS: (keyof AppScores)[] = [
|
|
55
54
|
"activity",
|
|
56
55
|
"maturity",
|
|
57
56
|
"learning",
|
|
@@ -61,7 +60,7 @@ export const SCORE_DIMENSIONS: (keyof Score)[] = [
|
|
|
61
60
|
];
|
|
62
61
|
|
|
63
62
|
/** Human-readable label for a score dimension. */
|
|
64
|
-
export const SCORE_LABELS: Record<keyof
|
|
63
|
+
export const SCORE_LABELS: Record<keyof AppScores, string> = {
|
|
65
64
|
activity: "Activity",
|
|
66
65
|
maturity: "Maturity",
|
|
67
66
|
learning: "Learning",
|
|
@@ -73,9 +72,9 @@ export const SCORE_LABELS: Record<keyof Score, string> = {
|
|
|
73
72
|
/**
|
|
74
73
|
* Reasoning copy per dimension, surfaced in the detail page so the
|
|
75
74
|
* numbers don't feel magical. These are intentionally short — the
|
|
76
|
-
* curation
|
|
75
|
+
* curation.notes on each item carries the longer story.
|
|
77
76
|
*/
|
|
78
|
-
export const SCORE_REASONING: Record<keyof
|
|
77
|
+
export const SCORE_REASONING: Record<keyof AppScores, string> = {
|
|
79
78
|
activity:
|
|
80
79
|
"Recent commits, open and merged PRs, issue response time, release cadence.",
|
|
81
80
|
maturity:
|
|
@@ -87,8 +86,5 @@ export const SCORE_REASONING: Record<keyof Score, string> = {
|
|
|
87
86
|
docs:
|
|
88
87
|
"README quality, architecture docs, contributing guide, examples, code comments.",
|
|
89
88
|
overall:
|
|
90
|
-
"Composite judgment — how likely this
|
|
89
|
+
"Composite judgment — how likely this item is to satisfy a curious developer.",
|
|
91
90
|
};
|
|
92
|
-
|
|
93
|
-
/** Re-export the `Score` type for callers that only need the lib API. */
|
|
94
|
-
export type { Score };
|