@grove-dev/astro 0.1.4 → 0.2.2
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/index.d.ts +4 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +56 -22
- package/dist/index.js.map +1 -1
- package/dist/layouts/index.d.ts +25 -0
- package/dist/layouts/index.d.ts.map +1 -0
- package/dist/layouts/index.js +25 -0
- package/dist/layouts/index.js.map +1 -0
- package/dist/lib/display.d.ts +33 -0
- package/dist/lib/display.d.ts.map +1 -0
- package/dist/lib/display.js +104 -0
- package/dist/lib/display.js.map +1 -0
- package/dist/lib/format.d.ts +30 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +73 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +26 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/lenses.d.ts +96 -0
- package/dist/lib/lenses.d.ts.map +1 -0
- package/dist/lib/lenses.js +181 -0
- package/dist/lib/lenses.js.map +1 -0
- package/dist/lib/repo.d.ts +30 -0
- package/dist/lib/repo.d.ts.map +1 -0
- package/dist/lib/repo.js +44 -0
- package/dist/lib/repo.js.map +1 -0
- package/dist/lib/scores.d.ts +38 -0
- package/dist/lib/scores.d.ts.map +1 -0
- package/dist/lib/scores.js +83 -0
- package/dist/lib/scores.js.map +1 -0
- package/dist/lib/search.d.ts +199 -0
- package/dist/lib/search.d.ts.map +1 -0
- package/dist/lib/search.js +357 -0
- package/dist/lib/search.js.map +1 -0
- package/dist/lib/taxonomy-counts.d.ts +25 -0
- package/dist/lib/taxonomy-counts.d.ts.map +1 -0
- package/dist/lib/taxonomy-counts.js +41 -0
- package/dist/lib/taxonomy-counts.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AppSection.astro +83 -0
- package/src/components/AppsIndexRow.astro +204 -0
- package/src/components/AppsPagination.astro +126 -0
- package/src/components/CategoryGrid.astro +86 -17
- package/src/components/ContributorsGrid.astro +149 -0
- package/src/components/DecisionRow.astro +89 -0
- package/src/components/ExploreByCategory.astro +67 -0
- package/src/components/ExploreByStack.astro +72 -0
- package/src/components/Hero.astro +246 -0
- package/src/components/Icon.astro +223 -0
- package/src/components/ItemCard.astro +289 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RefinePanel.astro +197 -0
- package/src/components/ScoreBars.astro +92 -22
- package/src/components/SmartLensTabs.astro +71 -0
- package/src/components/StackGrid.astro +132 -0
- package/src/components/WhyThisExists.astro +116 -0
- package/src/index.ts +61 -14
- package/src/layouts/BaseLayout.astro +227 -15
- package/src/layouts/Container.astro +57 -0
- package/src/layouts/Footer.astro +235 -0
- package/src/layouts/Header.astro +159 -0
- package/src/layouts/SectionHeader.astro +58 -0
- package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
- package/src/layouts/ThemeToggle.astro +110 -0
- package/src/layouts/astro-shim.d.ts +29 -0
- package/src/layouts/index.ts +24 -0
- package/src/lib/display.ts +118 -0
- package/src/lib/format.ts +65 -0
- package/src/lib/index.ts +26 -0
- package/src/lib/lenses.ts +241 -0
- package/src/lib/repo.ts +44 -0
- package/src/lib/scores.ts +94 -0
- package/src/lib/search.ts +481 -0
- package/src/lib/taxonomy-counts.ts +60 -0
- package/src/styles.css +366 -638
- package/templates/default/.astro/content.d.ts +155 -0
- package/templates/default/.astro/settings.json +5 -0
- package/templates/default/.astro/types.d.ts +2 -0
- package/templates/default/astro.config.mjs +8 -6
- package/templates/default/content/pages/about.md +66 -0
- package/templates/default/content/pages/methodology.md +103 -0
- package/templates/default/data/generated/records.full.json +8 -0
- package/templates/default/data/generated/records.index.json +7 -0
- package/templates/default/data/generated/records.json +8 -0
- package/templates/default/data/generated/site-config.json +31 -0
- package/templates/default/data/records/.gitkeep +0 -0
- package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +1 -0
- package/templates/default/dist/about/index.html +165 -0
- package/templates/default/dist/icons/platforms/android.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-dark.svg +1 -0
- package/templates/default/dist/icons/platforms/apple-light.svg +1 -0
- package/templates/default/dist/icons/platforms/chrome.svg +1 -0
- package/templates/default/dist/icons/platforms/chromeos.svg +5 -0
- package/templates/default/dist/icons/platforms/desktop.svg +4 -0
- package/templates/default/dist/icons/platforms/embedded.svg +5 -0
- package/templates/default/dist/icons/platforms/ios.svg +1 -0
- package/templates/default/dist/icons/platforms/linux.svg +1 -0
- package/templates/default/dist/icons/platforms/macos.svg +1 -0
- package/templates/default/dist/icons/platforms/ubuntu.svg +1 -0
- package/templates/default/dist/icons/platforms/web.svg +5 -0
- package/templates/default/dist/icons/platforms/windows.svg +6 -0
- package/templates/default/dist/icons/stacks/android.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-dark.svg +1 -0
- package/templates/default/dist/icons/stacks/apple-light.svg +1 -0
- package/templates/default/dist/icons/stacks/capacitor.svg +14 -0
- package/templates/default/dist/icons/stacks/dart.svg +1 -0
- package/templates/default/dist/icons/stacks/firebase.svg +1 -0
- package/templates/default/dist/icons/stacks/flutter.svg +1 -0
- package/templates/default/dist/icons/stacks/graphql.svg +1 -0
- package/templates/default/dist/icons/stacks/ionic.svg +4 -0
- package/templates/default/dist/icons/stacks/java.svg +1 -0
- package/templates/default/dist/icons/stacks/javascript.svg +1 -0
- package/templates/default/dist/icons/stacks/kotlin.svg +1 -0
- package/templates/default/dist/icons/stacks/mongodb.svg +1 -0
- package/templates/default/dist/icons/stacks/nodejs.svg +1 -0
- package/templates/default/dist/icons/stacks/python.svg +1 -0
- package/templates/default/dist/icons/stacks/react-native.svg +1 -0
- package/templates/default/dist/icons/stacks/react.svg +1 -0
- package/templates/default/dist/icons/stacks/rust.svg +1 -0
- package/templates/default/dist/icons/stacks/solidity.svg +1 -0
- package/templates/default/dist/icons/stacks/swift.svg +1 -0
- package/templates/default/dist/icons/stacks/tensorflow.svg +1 -0
- package/templates/default/dist/icons/stacks/typescript.svg +1 -0
- package/templates/default/dist/index.html +55 -0
- package/templates/default/dist/llms-full.txt +14 -0
- package/templates/default/dist/llms.txt +11 -0
- package/templates/default/dist/og-image.svg +48 -0
- package/templates/default/dist/projects/index.html +61 -0
- package/templates/default/dist/robots.txt +53 -0
- package/templates/default/dist/sitemap.xml +21 -0
- package/templates/default/dist/submit/index.html +69 -0
- package/templates/default/grove.config.ts +36 -0
- package/templates/default/node_modules/.bin/astro +21 -0
- package/templates/default/node_modules/.bin/astro-check +21 -0
- package/templates/default/node_modules/.bin/grove +21 -0
- package/templates/default/node_modules/.bin/jiti +21 -0
- package/templates/default/node_modules/.bin/prettier +21 -0
- package/templates/default/node_modules/.bin/rollup +21 -0
- package/templates/default/node_modules/.bin/tsc +21 -0
- package/templates/default/node_modules/.bin/tsserver +21 -0
- package/templates/default/node_modules/.bin/tsx +21 -0
- package/templates/default/node_modules/.bin/vite +21 -0
- package/templates/default/node_modules/.bin/yaml +21 -0
- package/templates/default/node_modules/.vite/deps/_metadata.json +79 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +433 -0
- package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +5 -0
- package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +9 -0
- package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +241 -0
- package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +1592 -0
- package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +186 -0
- package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +48 -0
- package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +6779 -0
- package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +33 -0
- package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +88 -0
- package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +72 -0
- package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +3757 -0
- package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +54 -0
- package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/package.json +3 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +213 -0
- package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +525 -0
- package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +1020 -0
- package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +7 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +147 -0
- package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +7 -0
- package/templates/default/package.json +6 -5
- package/templates/default/public/llms-full.txt +14 -0
- package/templates/default/public/llms.txt +11 -0
- package/templates/default/src/components/layout/BaseLayout.astro +10 -57
- package/templates/default/src/components/layout/Footer.astro +16 -134
- package/templates/default/src/components/layout/Header.astro +25 -126
- package/templates/default/src/data/records.ts +161 -0
- package/templates/default/src/lib/markdown.ts +171 -0
- package/templates/default/src/pages/about.astro +95 -429
- package/templates/default/src/pages/index.astro +258 -61
- package/templates/default/src/pages/projects/[slug].astro +480 -0
- package/templates/default/src/pages/projects/index.astro +193 -0
- package/templates/default/src/pages/sitemap.xml.ts +73 -61
- package/templates/default/src/pages/submit.astro +122 -411
- package/templates/default/src/styles/global.css +42 -68
- package/src/components/ActiveFilterChips.astro +0 -20
- package/src/components/DirectoryFilters.astro +0 -100
- package/src/components/DirectoryHero.astro +0 -32
- package/src/components/HealthBadge.astro +0 -12
- package/src/components/LensTabs.astro +0 -26
- package/src/components/MethodologyPanel.astro +0 -20
- package/src/components/Pagination.astro +0 -24
- package/src/components/ProjectDetail.astro +0 -166
- package/src/components/SubmitDraft.astro +0 -137
- package/templates/default/src/components/AppCard.astro +0 -248
- package/templates/default/src/components/AppSection.astro +0 -58
- package/templates/default/src/components/AppsIndexRow.astro +0 -230
- package/templates/default/src/components/AppsPagination.astro +0 -147
- package/templates/default/src/components/CategoryGrid.astro +0 -80
- package/templates/default/src/components/ContributorsGrid.astro +0 -105
- package/templates/default/src/components/DecisionRow.astro +0 -219
- package/templates/default/src/components/ExploreByCategory.astro +0 -74
- package/templates/default/src/components/ExploreByStack.astro +0 -102
- package/templates/default/src/components/Hero.astro +0 -176
- package/templates/default/src/components/MinimalAbout.astro +0 -57
- package/templates/default/src/components/OriginalCollection.astro +0 -144
- package/templates/default/src/components/RefinePanel.astro +0 -272
- package/templates/default/src/components/ScoreBars.astro +0 -72
- package/templates/default/src/components/SmartLensTabs.astro +0 -65
- package/templates/default/src/components/StackGrid.astro +0 -95
- package/templates/default/src/components/WhyThisExists.astro +0 -81
- package/templates/default/src/components/icons/Icon.astro +0 -148
- package/templates/default/src/components/layout/SectionHeader.astro +0 -42
- package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
- package/templates/default/src/data/apps.ts +0 -149
- package/templates/default/src/data/categories.ts +0 -27
- package/templates/default/src/data/config.ts +0 -28
- package/templates/default/src/data/contributors.ts +0 -96
- package/templates/default/src/data/stacks.ts +0 -47
- package/templates/default/src/data/stats.ts +0 -65
- package/templates/default/src/data/types.ts +0 -248
- package/templates/default/src/lib/apps-search.ts +0 -228
- package/templates/default/src/lib/format.ts +0 -36
- package/templates/default/src/lib/lenses.ts +0 -98
- package/templates/default/src/lib/repo.ts +0 -12
- package/templates/default/src/lib/scores.ts +0 -51
- package/templates/default/src/lib/taxonomy-counts.ts +0 -31
- package/templates/default/src/pages/apps/[slug].astro +0 -709
- package/templates/default/src/pages/apps/index.astro +0 -669
- package/templates/default/src/pages/contributors.astro +0 -142
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting helpers.
|
|
3
|
+
*
|
|
4
|
+
* Dependency-free — importable by both server-rendered Astro
|
|
5
|
+
* components and any future client-side script. No `Intl`-heavy
|
|
6
|
+
* calls that would explode the client bundle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format a number compactly: 1234 → "1.2k", 12345 → "12k", 79 → "79".
|
|
11
|
+
* Used for hero/header star counts and other large-num displays.
|
|
12
|
+
*/
|
|
13
|
+
export function compact(n: number): string {
|
|
14
|
+
if (n >= 1000) {
|
|
15
|
+
const k = n / 1000;
|
|
16
|
+
return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
|
|
17
|
+
}
|
|
18
|
+
return n.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format a star count compactly. Returns `null` when input is not a
|
|
23
|
+
* finite number, so callers can render "—" without an extra guard.
|
|
24
|
+
*/
|
|
25
|
+
export function formatStars(n: number | null | undefined): string | null {
|
|
26
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return null;
|
|
27
|
+
return compact(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format an ISO date as a human "Xd ago" / "Xmo ago" / "Xy ago" string.
|
|
32
|
+
* Returns "—" for missing or invalid input, and "today" for future
|
|
33
|
+
* dates (which can happen with bad upstream timestamps).
|
|
34
|
+
*/
|
|
35
|
+
export function formatRelative(date: string | null | undefined): string {
|
|
36
|
+
if (!date) return "—";
|
|
37
|
+
const d = new Date(date);
|
|
38
|
+
if (Number.isNaN(d.valueOf())) return "—";
|
|
39
|
+
const days = Math.floor((Date.now() - d.valueOf()) / (1000 * 60 * 60 * 24));
|
|
40
|
+
if (days <= 0) return "today";
|
|
41
|
+
if (days === 1) return "1d ago";
|
|
42
|
+
if (days < 30) return `${days}d ago`;
|
|
43
|
+
const months = Math.floor(days / 30);
|
|
44
|
+
if (months === 1) return "1mo ago";
|
|
45
|
+
if (months < 12) return `${months}mo ago`;
|
|
46
|
+
const years = Math.floor(months / 12);
|
|
47
|
+
if (years === 1) return "1y ago";
|
|
48
|
+
return `${years}y ago`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format an ISO date as a localized short date (e.g. "Jan 5, 2026").
|
|
53
|
+
* Returns the original string for unparseable input, or "—" when
|
|
54
|
+
* no value is provided.
|
|
55
|
+
*/
|
|
56
|
+
export function formatDate(date: string | null | undefined): string {
|
|
57
|
+
if (!date) return "—";
|
|
58
|
+
const d = new Date(date);
|
|
59
|
+
if (Number.isNaN(d.valueOf())) return date;
|
|
60
|
+
return d.toLocaleDateString("en-US", {
|
|
61
|
+
year: "numeric",
|
|
62
|
+
month: "short",
|
|
63
|
+
day: "numeric",
|
|
64
|
+
});
|
|
65
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @grove-dev/astro — framework-agnostic lib helpers.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports every helper that the showcase component library
|
|
5
|
+
* (and any future consumer) can use to:
|
|
6
|
+
*
|
|
7
|
+
* - parse GitHub repo URLs (`lib/repo`)
|
|
8
|
+
* - format counts, dates, stars (`lib/format`)
|
|
9
|
+
* - apply curated lenses / facets / search filters (`lib/lenses`,
|
|
10
|
+
* `lib/search`)
|
|
11
|
+
* - render score bars (`lib/scores`)
|
|
12
|
+
* - compute taxonomy counts at build time (`lib/taxonomy-counts`)
|
|
13
|
+
* - pretty-print slugs and label/lens/sort/status ids (`lib/display`)
|
|
14
|
+
*
|
|
15
|
+
* All helpers are pure, dependency-free, and typed against
|
|
16
|
+
* `@grove-dev/core` (specifically `ProjectRecord` for the index
|
|
17
|
+
* payload and the `Score` / `HealthStatus` unions).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export * from "./repo.js";
|
|
21
|
+
export * from "./format.js";
|
|
22
|
+
export * from "./lenses.js";
|
|
23
|
+
export * from "./search.js";
|
|
24
|
+
export * from "./scores.js";
|
|
25
|
+
export * from "./taxonomy-counts.js";
|
|
26
|
+
export * from "./display.js";
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated lenses for the list / discovery page.
|
|
3
|
+
*
|
|
4
|
+
* A lens is a single-select view that translates into a (possibly
|
|
5
|
+
* empty) URL search-param filter. The 6 lenses below cover the
|
|
6
|
+
* "what should I look at next?" mental shortcut:
|
|
7
|
+
*
|
|
8
|
+
* - "all" — no filter
|
|
9
|
+
* - "hot" — recent attention: stars ≥ 1000 OR pushed within 30d
|
|
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.)
|
|
14
|
+
*
|
|
15
|
+
* The lens id is what shows up in the URL as `?lens=...`. Filter
|
|
16
|
+
* resolution happens in `lib/search.ts` (`filterApps`).
|
|
17
|
+
*
|
|
18
|
+
* Keep this list in sync with `lib/search.ts` and any UI tabs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
HealthStatus,
|
|
23
|
+
ProjectRecord,
|
|
24
|
+
} from "@grove-dev/core";
|
|
25
|
+
|
|
26
|
+
export type LensId =
|
|
27
|
+
| "all"
|
|
28
|
+
| "hot"
|
|
29
|
+
| "new"
|
|
30
|
+
| "mature"
|
|
31
|
+
| "featured"
|
|
32
|
+
| "needs-review";
|
|
33
|
+
|
|
34
|
+
export interface LensDef {
|
|
35
|
+
id: LensId;
|
|
36
|
+
label: string;
|
|
37
|
+
description: string;
|
|
38
|
+
/**
|
|
39
|
+
* Returns true when the record matches this lens. Designed to
|
|
40
|
+
* operate on the index payload (`toIndexRecord`) shape — the
|
|
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);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const LENSES: LensDef[] = [
|
|
95
|
+
{
|
|
96
|
+
id: "all",
|
|
97
|
+
label: "All",
|
|
98
|
+
description: "Every record in the directory.",
|
|
99
|
+
match: () => true,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "hot",
|
|
103
|
+
label: "Trending",
|
|
104
|
+
description:
|
|
105
|
+
"Records with recent attention — stars ≥ 1000 or pushed within 30 days.",
|
|
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
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: "mature",
|
|
124
|
+
label: "Established",
|
|
125
|
+
description:
|
|
126
|
+
"Long-running projects — curated tier and last push > 365 days ago.",
|
|
127
|
+
match: (r) => {
|
|
128
|
+
const tier = r.health?.tier;
|
|
129
|
+
const pushed = daysSince(r.github?.pushedAt);
|
|
130
|
+
return tier === "curated" && pushed > 365;
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: "featured",
|
|
135
|
+
label: "Featured",
|
|
136
|
+
description: "Records curated with the “featured” label.",
|
|
137
|
+
match: (r) => hasLabel(r, "featured"),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "needs-review",
|
|
141
|
+
label: "Needs review",
|
|
142
|
+
description: "Cleanup candidates — stale, archived, or unmaintained.",
|
|
143
|
+
match: (r) => isCleanupCandidate(r),
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The lenses rendered as top-row tabs on the list page. Order
|
|
149
|
+
* matters — left to right. Keep this in sync with what the design
|
|
150
|
+
* calls for; URL deep-linking accepts any `LensId` even if it's not
|
|
151
|
+
* in the primary row.
|
|
152
|
+
*/
|
|
153
|
+
export const PRIMARY_LENSES: LensId[] = [
|
|
154
|
+
"all",
|
|
155
|
+
"hot",
|
|
156
|
+
"new",
|
|
157
|
+
"mature",
|
|
158
|
+
"featured",
|
|
159
|
+
"needs-review",
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
export function lensById(id: string | null | undefined): LensDef | undefined {
|
|
163
|
+
if (!id) return undefined;
|
|
164
|
+
return LENSES.find((l) => l.id === id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function isPrimaryLens(id: string | null | undefined): id is LensId {
|
|
168
|
+
if (!id) return false;
|
|
169
|
+
return PRIMARY_LENSES.includes(id as LensId);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resolve the active lens from URL search params. Returns the empty
|
|
174
|
+
* string when no `lens=...` is set, so callers can treat both
|
|
175
|
+
* "absent" and "unknown" identically.
|
|
176
|
+
*/
|
|
177
|
+
export function lensFromSearchParams(
|
|
178
|
+
params: URLSearchParams,
|
|
179
|
+
): LensId | "" {
|
|
180
|
+
const v = params.get("lens");
|
|
181
|
+
if (!v) return "";
|
|
182
|
+
if (LENSES.some((l) => l.id === v)) return v as LensId;
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `true` when the URL search params currently reflect this lens's
|
|
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.
|
|
190
|
+
*/
|
|
191
|
+
export function isLensActive(lensId: LensId, params: URLSearchParams): boolean {
|
|
192
|
+
if (lensId === "all") {
|
|
193
|
+
return lensFromSearchParams(params) === "" && !params.get("label");
|
|
194
|
+
}
|
|
195
|
+
return lensFromSearchParams(params) === lensId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build a `href` for a lens tab. Selecting a lens resets the other
|
|
200
|
+
* "view" params (label, page) and applies only the chosen lens's
|
|
201
|
+
* filter. Other filters (q, stack, platform, category, sort) are
|
|
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 `/`.
|
|
207
|
+
*/
|
|
208
|
+
export function hrefForLens(
|
|
209
|
+
lensId: LensId,
|
|
210
|
+
params: URLSearchParams,
|
|
211
|
+
pathPrefix: string = "/",
|
|
212
|
+
): string {
|
|
213
|
+
const sp = new URLSearchParams(params);
|
|
214
|
+
sp.delete("lens");
|
|
215
|
+
sp.delete("label");
|
|
216
|
+
sp.delete("page");
|
|
217
|
+
if (lensId && lensId !== "all") {
|
|
218
|
+
sp.set("lens", lensId);
|
|
219
|
+
}
|
|
220
|
+
const qs = sp.toString();
|
|
221
|
+
const base = pathPrefix.replace(/\/$/, "");
|
|
222
|
+
return qs ? `${base}?${qs}` : base || "/";
|
|
223
|
+
}
|
|
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
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Used in:
|
|
10
|
+
* - ItemCard / AppCard avatar fallback (owner avatar via GitHub
|
|
11
|
+
* avatars API).
|
|
12
|
+
* - Detail page (showing owner + repo name in breadcrumb / header).
|
|
13
|
+
* - Anywhere we need to display "@owner / repo" or link to the owner.
|
|
14
|
+
*/
|
|
15
|
+
export function getOwnerAndRepoFromRepoUrl(
|
|
16
|
+
repoUrl: string | null | undefined,
|
|
17
|
+
): { owner: string; repo: string } | null {
|
|
18
|
+
if (!repoUrl || !repoUrl.includes("github.com/")) return null;
|
|
19
|
+
const m = repoUrl.match(/github\.com\/([^/]+)\/([^/?#]+)/);
|
|
20
|
+
if (!m) return null;
|
|
21
|
+
return {
|
|
22
|
+
owner: m[1],
|
|
23
|
+
repo: m[2].replace(/\.git$/, ""),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build a GitHub avatar URL for a given owner. Defaults to size 80
|
|
29
|
+
* which is the smallest "high quality" size in GitHub's avatar CDN.
|
|
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).
|
|
36
|
+
*/
|
|
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
|
+
}
|
|
43
|
+
return `https://avatars.githubusercontent.com/${owner}?v=4&s=${size}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Score-dimension utilities.
|
|
3
|
+
*
|
|
4
|
+
* Operates on the `scores` field of a `ProjectRecord` (or any
|
|
5
|
+
* object that has the same shape — defined as `Score` in
|
|
6
|
+
* `@grove-dev/core`). All helpers are dependency-free.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Score } from "@grove-dev/core";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a single score (0-100) into a 0-5 "tier" used by the UI:
|
|
13
|
+
*
|
|
14
|
+
* 0-19 → 0 (very low)
|
|
15
|
+
* 20-39 → 1 (low)
|
|
16
|
+
* 40-59 → 2 (medium)
|
|
17
|
+
* 60-79 → 3 (high)
|
|
18
|
+
* 80-100 → 4 (very high)
|
|
19
|
+
*
|
|
20
|
+
* The list page renders each score as a 4-cell bar; this gives
|
|
21
|
+
* the visual a stable shape regardless of the underlying number.
|
|
22
|
+
*/
|
|
23
|
+
export function scoreTier(n: number): 0 | 1 | 2 | 3 | 4 {
|
|
24
|
+
if (n < 20) return 0;
|
|
25
|
+
if (n < 40) return 1;
|
|
26
|
+
if (n < 60) return 2;
|
|
27
|
+
if (n < 80) return 3;
|
|
28
|
+
return 4;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Compact textual label for a score tier (used in tooltips). */
|
|
32
|
+
export function scoreTierLabel(n: number): string {
|
|
33
|
+
switch (scoreTier(n)) {
|
|
34
|
+
case 0:
|
|
35
|
+
return "Very low";
|
|
36
|
+
case 1:
|
|
37
|
+
return "Low";
|
|
38
|
+
case 2:
|
|
39
|
+
return "Medium";
|
|
40
|
+
case 3:
|
|
41
|
+
return "High";
|
|
42
|
+
case 4:
|
|
43
|
+
return "Very high";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Short numeric label used inline next to a score bar, e.g. "82". */
|
|
48
|
+
export function scoreLabel(n: number | undefined): string {
|
|
49
|
+
if (typeof n !== "number") return "—";
|
|
50
|
+
return Math.round(n).toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Order of score dimensions, used by both list and detail. */
|
|
54
|
+
export const SCORE_DIMENSIONS: (keyof Score)[] = [
|
|
55
|
+
"activity",
|
|
56
|
+
"maturity",
|
|
57
|
+
"learning",
|
|
58
|
+
"contribution",
|
|
59
|
+
"docs",
|
|
60
|
+
"overall",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
/** Human-readable label for a score dimension. */
|
|
64
|
+
export const SCORE_LABELS: Record<keyof Score, string> = {
|
|
65
|
+
activity: "Activity",
|
|
66
|
+
maturity: "Maturity",
|
|
67
|
+
learning: "Learning",
|
|
68
|
+
contribution: "Contribution",
|
|
69
|
+
docs: "Docs",
|
|
70
|
+
overall: "Overall",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reasoning copy per dimension, surfaced in the detail page so the
|
|
75
|
+
* numbers don't feel magical. These are intentionally short — the
|
|
76
|
+
* curation notes on each record carry the longer story.
|
|
77
|
+
*/
|
|
78
|
+
export const SCORE_REASONING: Record<keyof Score, string> = {
|
|
79
|
+
activity:
|
|
80
|
+
"Recent commits, open and merged PRs, issue response time, release cadence.",
|
|
81
|
+
maturity:
|
|
82
|
+
"Repo age, stable structure, real-world production usage, contributor stability.",
|
|
83
|
+
learning:
|
|
84
|
+
"Codebase readability, useful architecture patterns, comment quality, examples.",
|
|
85
|
+
contribution:
|
|
86
|
+
"Open issues, recent merged PRs from new contributors, maintainer responsiveness, contribution guide.",
|
|
87
|
+
docs:
|
|
88
|
+
"README quality, architecture docs, contributing guide, examples, code comments.",
|
|
89
|
+
overall:
|
|
90
|
+
"Composite judgment — how likely this record is to satisfy a curious developer.",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Re-export the `Score` type for callers that only need the lib API. */
|
|
94
|
+
export type { Score };
|