@grove-dev/astro 0.1.5 → 0.2.5

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.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +4 -25
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +56 -22
  5. package/dist/index.js.map +1 -1
  6. package/dist/layouts/index.d.ts +25 -0
  7. package/dist/layouts/index.d.ts.map +1 -0
  8. package/dist/layouts/index.js +25 -0
  9. package/dist/layouts/index.js.map +1 -0
  10. package/dist/lib/display.d.ts +33 -0
  11. package/dist/lib/display.d.ts.map +1 -0
  12. package/dist/lib/display.js +104 -0
  13. package/dist/lib/display.js.map +1 -0
  14. package/dist/lib/format.d.ts +30 -0
  15. package/dist/lib/format.d.ts.map +1 -0
  16. package/dist/lib/format.js +73 -0
  17. package/dist/lib/format.js.map +1 -0
  18. package/dist/lib/index.d.ts +26 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/index.js +26 -0
  21. package/dist/lib/index.js.map +1 -0
  22. package/dist/lib/lenses.d.ts +96 -0
  23. package/dist/lib/lenses.d.ts.map +1 -0
  24. package/dist/lib/lenses.js +181 -0
  25. package/dist/lib/lenses.js.map +1 -0
  26. package/dist/lib/repo.d.ts +30 -0
  27. package/dist/lib/repo.d.ts.map +1 -0
  28. package/dist/lib/repo.js +44 -0
  29. package/dist/lib/repo.js.map +1 -0
  30. package/dist/lib/scores.d.ts +38 -0
  31. package/dist/lib/scores.d.ts.map +1 -0
  32. package/dist/lib/scores.js +83 -0
  33. package/dist/lib/scores.js.map +1 -0
  34. package/dist/lib/search.d.ts +199 -0
  35. package/dist/lib/search.d.ts.map +1 -0
  36. package/dist/lib/search.js +357 -0
  37. package/dist/lib/search.js.map +1 -0
  38. package/dist/lib/taxonomy-counts.d.ts +25 -0
  39. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  40. package/dist/lib/taxonomy-counts.js +41 -0
  41. package/dist/lib/taxonomy-counts.js.map +1 -0
  42. package/package.json +7 -7
  43. package/src/components/AppsIndexRow.astro +204 -0
  44. package/src/components/AppsPagination.astro +126 -0
  45. package/src/components/CategoryGrid.astro +86 -17
  46. package/src/components/ContributorsGrid.astro +149 -0
  47. package/src/components/DecisionRow.astro +89 -0
  48. package/src/components/ExploreByCategory.astro +67 -0
  49. package/src/components/ExploreByStack.astro +72 -0
  50. package/src/components/Hero.astro +246 -0
  51. package/src/components/Icon.astro +223 -0
  52. package/src/components/ItemCard.astro +306 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RecordSection.astro +88 -0
  56. package/src/components/RefinePanel.astro +197 -0
  57. package/src/components/ScoreBars.astro +92 -22
  58. package/src/components/SmartLensTabs.astro +71 -0
  59. package/src/components/StackGrid.astro +132 -0
  60. package/src/components/WhyThisExists.astro +116 -0
  61. package/src/index.ts +61 -14
  62. package/src/layouts/BaseLayout.astro +227 -15
  63. package/src/layouts/Container.astro +57 -0
  64. package/src/layouts/Footer.astro +235 -0
  65. package/src/layouts/Header.astro +159 -0
  66. package/src/layouts/SectionHeader.astro +58 -0
  67. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  68. package/src/layouts/ThemeToggle.astro +110 -0
  69. package/src/layouts/astro-shim.d.ts +29 -0
  70. package/src/layouts/index.ts +24 -0
  71. package/src/lib/display.ts +118 -0
  72. package/src/lib/format.ts +65 -0
  73. package/src/lib/index.ts +26 -0
  74. package/src/lib/lenses.ts +241 -0
  75. package/src/lib/repo.ts +44 -0
  76. package/src/lib/scores.ts +94 -0
  77. package/src/lib/search.ts +481 -0
  78. package/src/lib/taxonomy-counts.ts +60 -0
  79. package/src/styles.css +366 -638
  80. package/templates/default/astro.config.mjs +8 -6
  81. package/templates/default/content/pages/about.md +66 -0
  82. package/templates/default/content/pages/methodology.md +103 -0
  83. package/templates/default/data/generated/records.full.json +8 -0
  84. package/templates/default/data/generated/records.index.json +7 -0
  85. package/templates/default/data/generated/records.json +8 -0
  86. package/templates/default/data/generated/site-config.json +31 -0
  87. package/templates/default/data/records/.gitkeep +0 -0
  88. package/templates/default/grove.config.ts +36 -0
  89. package/templates/default/package.json +6 -5
  90. package/templates/default/public/llms-full.txt +14 -0
  91. package/templates/default/public/llms.txt +11 -0
  92. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  93. package/templates/default/src/components/layout/Footer.astro +16 -134
  94. package/templates/default/src/components/layout/Header.astro +25 -126
  95. package/templates/default/src/data/records.ts +193 -0
  96. package/templates/default/src/lib/markdown.ts +171 -0
  97. package/templates/default/src/pages/about.astro +95 -429
  98. package/templates/default/src/pages/index.astro +263 -61
  99. package/templates/default/src/pages/projects/[slug].astro +480 -0
  100. package/templates/default/src/pages/projects/index.astro +193 -0
  101. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  102. package/templates/default/src/pages/submit.astro +122 -411
  103. package/templates/default/src/styles/global.css +42 -68
  104. package/src/components/ActiveFilterChips.astro +0 -20
  105. package/src/components/DirectoryFilters.astro +0 -100
  106. package/src/components/DirectoryHero.astro +0 -32
  107. package/src/components/HealthBadge.astro +0 -12
  108. package/src/components/LensTabs.astro +0 -26
  109. package/src/components/MethodologyPanel.astro +0 -20
  110. package/src/components/Pagination.astro +0 -24
  111. package/src/components/ProjectDetail.astro +0 -166
  112. package/src/components/SubmitDraft.astro +0 -137
  113. package/templates/default/src/components/AppCard.astro +0 -248
  114. package/templates/default/src/components/AppSection.astro +0 -58
  115. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  116. package/templates/default/src/components/AppsPagination.astro +0 -147
  117. package/templates/default/src/components/CategoryGrid.astro +0 -80
  118. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  119. package/templates/default/src/components/DecisionRow.astro +0 -219
  120. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  121. package/templates/default/src/components/ExploreByStack.astro +0 -102
  122. package/templates/default/src/components/Hero.astro +0 -176
  123. package/templates/default/src/components/MinimalAbout.astro +0 -57
  124. package/templates/default/src/components/OriginalCollection.astro +0 -144
  125. package/templates/default/src/components/RefinePanel.astro +0 -272
  126. package/templates/default/src/components/ScoreBars.astro +0 -72
  127. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  128. package/templates/default/src/components/StackGrid.astro +0 -95
  129. package/templates/default/src/components/WhyThisExists.astro +0 -81
  130. package/templates/default/src/components/icons/Icon.astro +0 -148
  131. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  132. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  133. package/templates/default/src/data/apps.ts +0 -149
  134. package/templates/default/src/data/categories.ts +0 -27
  135. package/templates/default/src/data/config.ts +0 -28
  136. package/templates/default/src/data/contributors.ts +0 -96
  137. package/templates/default/src/data/stacks.ts +0 -47
  138. package/templates/default/src/data/stats.ts +0 -65
  139. package/templates/default/src/data/types.ts +0 -248
  140. package/templates/default/src/lib/apps-search.ts +0 -228
  141. package/templates/default/src/lib/format.ts +0 -36
  142. package/templates/default/src/lib/lenses.ts +0 -98
  143. package/templates/default/src/lib/repo.ts +0 -12
  144. package/templates/default/src/lib/scores.ts +0 -51
  145. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  146. package/templates/default/src/pages/apps/[slug].astro +0 -709
  147. package/templates/default/src/pages/apps/index.astro +0 -669
  148. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,248 +0,0 @@
1
- export type AppStatus = "active" | "quiet" | "stale" | "archived" | "unknown";
2
- export type AppLabel = "new" | "hot" | "mature" | "featured";
3
-
4
- // ── Curation: what kind of app is this? ──────────────────────────────
5
- /**
6
- * `projectType` answers "is this a real, production-grade codebase
7
- * or a demo?" — the most-asked filter for anyone scanning a list.
8
- */
9
- export type ProjectType =
10
- | "real-app" // Actually used by real people in production
11
- | "production" // Commercial product with open codebase
12
- | "reference" // Showcase of patterns/architecture, not a real product
13
- | "demo" // Demo / proof-of-concept
14
- | "template"; // Starter template others fork
15
-
16
- export type Difficulty = "beginner" | "intermediate" | "advanced";
17
- export type CodebaseSize = "small" | "medium" | "large" | "huge";
18
-
19
- // ── Scoring: 6-dimension, 0-100 each ─────────────────────────────────
20
- /**
21
- * Six orthogonal scores that drive the "decision row" UI.
22
- * - activity: how alive is the repo (commits, PRs, issues responded)
23
- * - maturity: how established (age, stability, real usage)
24
- * - learning: how good for learning (readable code, useful patterns)
25
- * - contribution: how welcoming to new contributors
26
- * - docs: how good the documentation is
27
- * - overall: composite judgment
28
- *
29
- * Scores are curator-assigned integers. They are NOT computed from
30
- * any algorithm — see `curation.notes` for the reasoning chain.
31
- */
32
- export type AppScores = {
33
- activity: number;
34
- maturity: number;
35
- learning: number;
36
- contribution: number;
37
- docs: number;
38
- overall: number;
39
- };
40
-
41
- export type GithubRepositoryMetadata = {
42
- full_name?: string;
43
- html_url?: string;
44
- homepage?: string | null;
45
- description?: string | null;
46
- fork?: boolean;
47
- archived?: boolean;
48
- disabled?: boolean;
49
- private?: boolean;
50
- visibility?: string;
51
- default_branch?: string;
52
- language?: string | null;
53
- topics?: string[];
54
- license?: {
55
- key?: string | null;
56
- name?: string | null;
57
- spdx_id?: string | null;
58
- url?: string | null;
59
- } | null;
60
- stargazers_count?: number;
61
- watchers_count?: number;
62
- forks_count?: number;
63
- open_issues_count?: number;
64
- subscribers_count?: number;
65
- size?: number;
66
- created_at?: string | null;
67
- updated_at?: string | null;
68
- pushed_at?: string | null;
69
- };
70
-
71
- export type AppHealth = {
72
- status?: string;
73
- tier?: string;
74
- visibility?: string;
75
- activityScore?: number;
76
- maintenanceScore?: number;
77
- metadataCompleteness?: number;
78
- cleanupCandidate?: boolean;
79
- staleReason?: string | null;
80
- };
81
-
82
- export type DistributionChannel = {
83
- type:
84
- | "app-store"
85
- | "play-store"
86
- | "fdroid"
87
- | "github-releases"
88
- | "apk"
89
- | "testflight"
90
- | "website"
91
- | "snapcraft"
92
- | "flathub"
93
- | "microsoft-store"
94
- | "other";
95
- platform?: string;
96
- label?: string;
97
- url: string;
98
- verified?: boolean;
99
- notes?: string;
100
- };
101
-
102
- export type AppDistribution = {
103
- channels?: DistributionChannel[];
104
- };
105
-
106
- // ── Curation provenance: was this human-reviewed? ────────────────────
107
- /**
108
- * Every curated field needs a chain of trust. `reviewed: true` means
109
- * a human looked at it and signed off. `reviewed: false` means it's
110
- * auto-generated (e.g. by AI from public repo metadata) and pending
111
- * curator review. The UI surfaces this distinction.
112
- */
113
- export type CurationInfo = {
114
- reviewed: boolean;
115
- /** Curator handle, e.g. "@username" or "SITECONFIG_NAME_PLACEHOLDER editors". */
116
- by?: string;
117
- /** ISO date the curation was authored or last reviewed. */
118
- date?: string;
119
- /** Free-text: where did the data come from? what's uncertain? */
120
- notes?: string;
121
- };
122
-
123
- // ── The app record ──────────────────────────────────────────────────
124
- /**
125
- * One open-source app entry.
126
- *
127
- * Fields split into two layers:
128
- *
129
- * 1. **Required discovery data** — name, description, repo, stack,
130
- * platforms, category. Without these, the app can't be listed.
131
- *
132
- * 2. **Optional curation data** — everything under the `// ── Curation`
133
- * block. New apps start sparse. The 5 anchor apps are fully curated.
134
- * The list/detail UI gracefully degrades when fields are missing.
135
- */
136
- export type OpenSourceApp = {
137
- // ── Identity ──
138
- slug: string;
139
- name: string;
140
- description: string;
141
- repoUrl: string;
142
- homepageUrl?: string;
143
-
144
- // ── Basic taxonomy ──
145
- /** Primary stack, e.g. "Flutter" */
146
- stack: string;
147
- /** Additional stacks used in the codebase. */
148
- stacks?: string[];
149
- platforms: string[];
150
- distribution?: AppDistribution;
151
- category: string;
152
- tags?: string[];
153
-
154
- // ── Public signals ──
155
- /** Direct URL to a square logo. Falls back to GitHub owner avatar. */
156
- logoUrl?: string;
157
- stars?: number;
158
- license?: string;
159
- status?: AppStatus;
160
- addedAt?: string;
161
- lastCommitAt?: string;
162
- labels?: AppLabel[];
163
-
164
- // ── Curation: project shape ──
165
- /** What kind of app is this? (real-app / production / reference / …) */
166
- projectType?: ProjectType;
167
- /** What state-management pattern does it use? "Riverpod", "BLoC", etc. */
168
- stateManagement?: string;
169
- /** What backend / data layer? "REST", "Firebase", "PostgreSQL", … */
170
- backend?: string;
171
- /** Architecture style. "Feature-based", "Clean", "MVC", "Layered", … */
172
- architecture?: string;
173
- /** How hard is it to read / contribute? */
174
- difficulty?: Difficulty;
175
- /** Approximate size, by curator judgment. */
176
- codebaseSize?: CodebaseSize;
177
-
178
- // ── Curation: text signals ──
179
- /**
180
- * 2-5 short phrases: "Studying offline-first architecture",
181
- * "REST API integration patterns", etc. Hand-written.
182
- */
183
- bestFor?: string[];
184
- /**
185
- * Why is this app listed? Curator's one-liner per angle.
186
- * E.g. ["Most starred Flutter app", "Real production deployment"].
187
- */
188
- whyListed?: string[];
189
- /**
190
- * Caveats / risks. Honest downsides, not marketing.
191
- * E.g. ["Large codebase", "Sparse documentation", "Archived"].
192
- */
193
- caveats?: string[];
194
-
195
- // ── Curation: contribution links ──
196
- /**
197
- * `true` if the repo has a "good first issue" label,
198
- * or a direct URL to a filtered list of them.
199
- */
200
- goodFirstIssues?: boolean | string;
201
- /** `true` if the repo has a CONTRIBUTING.md, or a direct URL. */
202
- contributionGuide?: boolean | string;
203
-
204
- // ── Curation: launch (Product-Hunt-style, optional) ──
205
- /**
206
- * For recently-launched apps, the username of the person who
207
- * submitted the launch entry. Long-running apps leave this empty.
208
- */
209
- launchedBy?: string;
210
- /**
211
- * What the maintainer wants from this launch:
212
- * "Feedback", "Contributors", "Users", "Stars", etc.
213
- */
214
- launchAsk?: string[];
215
-
216
- /**
217
- * Curated lenses — the curated "tabs" on the list page. These are
218
- * hand-assigned per app by a curator; not derived from scores.
219
- * Valid values: "good-to-learn" | "production-like" |
220
- * "beginner-friendly" | "contribution-ready" | "launches".
221
- * The label-based lenses (new / hot / mature) live in `labels`.
222
- */
223
- lenses?: string[];
224
-
225
- // ── Curation: scores ──
226
- scores?: AppScores;
227
-
228
- // ── Curation: provenance ──
229
- curation?: CurationInfo;
230
-
231
- // ── Final schema raw blocks ────────────────────────────────────────
232
- github?: {
233
- repository?: GithubRepositoryMetadata;
234
- languages?: Record<string, number>;
235
- latestRelease?: Record<string, unknown> | null;
236
- activity?: {
237
- monthlyCommits?: Array<number | { month: string; commits: number }>;
238
- totalCommitsKnown?: number;
239
- contributorsKnown?: number;
240
- openPullRequests?: number;
241
- };
242
- files?: Record<string, boolean>;
243
- labels?: Array<Record<string, unknown>>;
244
- sync?: Record<string, unknown>;
245
- };
246
- health?: AppHealth;
247
- tier?: string;
248
- };
@@ -1,228 +0,0 @@
1
- /**
2
- * Filter / sort / pagination helpers for the apps list page.
3
- *
4
- * This file is a thin proxy over `@grove-dev/ui` and `@grove-dev/core`
5
- * — the actual business logic lives there. The proxy keeps the
6
- * `OpenSourceApp` shape (defined in `data/types.ts`) compatible
7
- * with the core `AppRecord` shape.
8
- */
9
- import type { OpenSourceApp } from "../data/types";
10
-
11
- export const PAGE_SIZE = 20;
12
-
13
- export const SORT_OPTIONS = [
14
- { value: "recently-updated", label: "Recently updated" },
15
- { value: "most-starred", label: "Most starred" },
16
- { value: "recently-added", label: "Recently added" },
17
- { value: "most-mature", label: "Most mature" },
18
- { value: "alphabetical", label: "Alphabetical" },
19
- ] as const;
20
-
21
- export type AppsSort = (typeof SORT_OPTIONS)[number]["value"];
22
-
23
- export type AppsFilters = {
24
- q?: string;
25
- stacks?: string[];
26
- platforms?: string[];
27
- categories?: string[];
28
- labels?: string[];
29
- licenses?: string[];
30
- statuses?: string[];
31
- lens?: string;
32
- sort?: AppsSort;
33
- page?: number;
34
- density?: "comfortable" | "compact";
35
- };
36
-
37
- const KEYS = {
38
- q: "q",
39
- stacks: "stack",
40
- platforms: "platform",
41
- categories: "category",
42
- labels: "label",
43
- licenses: "license",
44
- statuses: "status",
45
- lens: "lens",
46
- sort: "sort",
47
- page: "page",
48
- density: "density",
49
- } as const;
50
-
51
- const DEFAULT_SORT: AppsSort = "recently-updated";
52
- const DEFAULT_PAGE = 1;
53
-
54
- export function filtersFromSearchParams(sp: URLSearchParams): AppsFilters {
55
- const rawSort = sp.get(KEYS.sort);
56
- const sort: AppsSort | undefined =
57
- rawSort && SORT_OPTIONS.some((o) => o.value === rawSort) ? (rawSort as AppsSort) : undefined;
58
- const rawPage = Number(sp.get(KEYS.page));
59
- const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : undefined;
60
- const rawDensity = sp.get(KEYS.density);
61
- const density = rawDensity === "compact" || rawDensity === "comfortable" ? rawDensity : undefined;
62
- return {
63
- q: sp.get(KEYS.q) ?? undefined,
64
- stacks: sp.getAll(KEYS.stacks).filter(Boolean),
65
- platforms: sp.getAll(KEYS.platforms).filter(Boolean),
66
- categories: sp.getAll(KEYS.categories).filter(Boolean),
67
- labels: sp.getAll(KEYS.labels).filter(Boolean),
68
- licenses: sp.getAll(KEYS.licenses).filter(Boolean),
69
- statuses: sp.getAll(KEYS.statuses).flatMap((s) => s.split(",")).filter(Boolean),
70
- lens: sp.get(KEYS.lens) ?? undefined,
71
- sort,
72
- page,
73
- density,
74
- };
75
- }
76
-
77
- export function searchParamsFromFilters(f: AppsFilters): URLSearchParams {
78
- const sp = new URLSearchParams();
79
- if (f.q) sp.set(KEYS.q, f.q);
80
- for (const v of f.stacks ?? []) sp.append(KEYS.stacks, v);
81
- for (const v of f.platforms ?? []) sp.append(KEYS.platforms, v);
82
- for (const v of f.categories ?? []) sp.append(KEYS.categories, v);
83
- for (const v of f.labels ?? []) sp.append(KEYS.labels, v);
84
- for (const v of f.licenses ?? []) sp.append(KEYS.licenses, v);
85
- if (f.statuses?.length) sp.set(KEYS.statuses, f.statuses.join(","));
86
- if (f.lens) sp.set(KEYS.lens, f.lens);
87
- if (f.sort && f.sort !== DEFAULT_SORT) sp.set(KEYS.sort, f.sort);
88
- if (f.page && f.page !== DEFAULT_PAGE) sp.set(KEYS.page, String(f.page));
89
- if (f.density && f.density !== "comfortable") sp.set(KEYS.density, f.density);
90
- return sp;
91
- }
92
-
93
- export function hasAnyFilter(f: AppsFilters): boolean {
94
- if (f.q && f.q.trim()) return true;
95
- if (f.lens) return true;
96
- return Boolean(
97
- (f.stacks?.length ?? 0) +
98
- (f.platforms?.length ?? 0) +
99
- (f.categories?.length ?? 0) +
100
- (f.labels?.length ?? 0) +
101
- (f.licenses?.length ?? 0) +
102
- (f.statuses?.length ?? 0),
103
- );
104
- }
105
-
106
- export function applySort(apps: OpenSourceApp[], sort: AppsSort): OpenSourceApp[] {
107
- const arr = apps.slice();
108
- const ts = (s?: string | null): number => (s ? new Date(s).valueOf() : 0);
109
- switch (sort) {
110
- case "most-starred":
111
- arr.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
112
- break;
113
- case "recently-updated":
114
- arr.sort((a, b) => ts(b.lastCommitAt) - ts(a.lastCommitAt));
115
- break;
116
- case "recently-added":
117
- arr.sort((a, b) => ts(b.addedAt) - ts(a.addedAt));
118
- break;
119
- case "most-mature":
120
- arr.sort((a, b) => (b.stars ?? 0) - (a.stars ?? 0));
121
- break;
122
- case "alphabetical":
123
- arr.sort((a, b) => a.name.localeCompare(b.name));
124
- break;
125
- }
126
- return arr;
127
- }
128
-
129
- export function paginate<T>(items: T[], page: number, pageSize = PAGE_SIZE): T[] {
130
- const start = (page - 1) * pageSize;
131
- return items.slice(start, start + pageSize);
132
- }
133
-
134
- export function totalPages(itemCount: number, pageSize = PAGE_SIZE): number {
135
- return Math.max(1, Math.ceil(itemCount / pageSize));
136
- }
137
-
138
- export function effectiveSort(f: AppsFilters): AppsSort {
139
- return f.sort ?? DEFAULT_SORT;
140
- }
141
- export function effectivePage(f: AppsFilters): number {
142
- return f.page ?? DEFAULT_PAGE;
143
- }
144
- export function effectiveDensity(f: AppsFilters): "comfortable" | "compact" {
145
- return f.density ?? "comfortable";
146
- }
147
-
148
- export function filterApps(apps: OpenSourceApp[], f: AppsFilters): OpenSourceApp[] {
149
- const q = f.q?.trim().toLowerCase();
150
- const stacks = f.stacks?.length ? new Set(f.stacks) : null;
151
- const platforms = f.platforms?.length ? new Set(f.platforms) : null;
152
- const categories = f.categories?.length ? new Set(f.categories) : null;
153
- const labels = f.labels?.length ? new Set(f.labels) : null;
154
- const licenses = f.licenses?.length ? new Set(f.licenses) : null;
155
- const statuses = f.statuses?.length ? new Set(f.statuses) : null;
156
-
157
- return apps.filter((a) => {
158
- if (q) {
159
- const ownerMatch = /github\.com\/([^/]+)\//.exec(a.repoUrl)?.[1]?.toLowerCase() ?? "";
160
- const haystack = [a.name, ownerMatch, a.description, a.category].join(" ").toLowerCase();
161
- if (!haystack.includes(q)) return false;
162
- }
163
- if (stacks) {
164
- const allStacks = [a.stack, ...(a.stacks ?? [])];
165
- if (!allStacks.some((s) => stacks.has(s))) return false;
166
- }
167
- if (platforms) {
168
- if (!a.platforms.some((p) => platforms.has(p))) return false;
169
- }
170
- if (categories) {
171
- if (!categories.has(a.category)) return false;
172
- }
173
- if (labels) {
174
- if (!a.labels?.some((l) => labels.has(l))) return false;
175
- }
176
- if (licenses) {
177
- if (!a.license || !licenses.has(a.license)) return false;
178
- }
179
- if (statuses) {
180
- if (!a.status || !statuses.has(a.status)) return false;
181
- }
182
- if (f.lens) {
183
- if (!a.lenses?.includes(f.lens)) return false;
184
- }
185
- return true;
186
- });
187
- }
188
-
189
- export function activeFilterChips(
190
- f: AppsFilters,
191
- ): { key: keyof AppsFilters; value: string; label: string }[] {
192
- const out: { key: keyof AppsFilters; value: string; label: string }[] = [];
193
- if (f.q && f.q.trim()) out.push({ key: "q", value: "", label: `“${f.q}”` });
194
- for (const v of f.stacks ?? []) out.push({ key: "stacks", value: v, label: `stack: ${v}` });
195
- for (const v of f.platforms ?? []) out.push({ key: "platforms", value: v, label: `platform: ${v}` });
196
- for (const v of f.categories ?? []) out.push({ key: "categories", value: v, label: `category: ${v}` });
197
- for (const v of f.labels ?? []) out.push({ key: "labels", value: v, label: v });
198
- for (const v of f.licenses ?? []) out.push({ key: "licenses", value: v, label: `license: ${v}` });
199
- if (f.lens) out.push({ key: "lens", value: f.lens, label: `lens: ${f.lens}` });
200
- return out;
201
- }
202
-
203
- export function buildFacets(apps: OpenSourceApp[]) {
204
- const counts = {
205
- stack: new Map<string, number>(),
206
- platform: new Map<string, number>(),
207
- category: new Map<string, number>(),
208
- label: new Map<string, number>(),
209
- license: new Map<string, number>(),
210
- };
211
- for (const a of apps) {
212
- const allStacks = new Set([a.stack, ...(a.stacks ?? [])]);
213
- for (const s of allStacks) counts.stack.set(s, (counts.stack.get(s) ?? 0) + 1);
214
- for (const p of a.platforms) counts.platform.set(p, (counts.platform.get(p) ?? 0) + 1);
215
- counts.category.set(a.category, (counts.category.get(a.category) ?? 0) + 1);
216
- for (const l of a.labels ?? []) counts.label.set(l, (counts.label.get(l) ?? 0) + 1);
217
- if (a.license) counts.license.set(a.license, (counts.license.get(a.license) ?? 0) + 1);
218
- }
219
- const sortByCountThenName = (m: Map<string, number>) =>
220
- [...m.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([value, count]) => ({ value, count }));
221
- return {
222
- stacks: sortByCountThenName(counts.stack),
223
- platforms: sortByCountThenName(counts.platform),
224
- categories: sortByCountThenName(counts.category),
225
- labels: sortByCountThenName(counts.label),
226
- licenses: sortByCountThenName(counts.license),
227
- };
228
- }
@@ -1,36 +0,0 @@
1
- /** Number/date formatting helpers used across the templates. */
2
- export function compact(n: number): string {
3
- if (n >= 1000) {
4
- const k = n / 1000;
5
- return (k >= 10 ? Math.round(k).toString() : k.toFixed(1)) + "k";
6
- }
7
- return n.toString();
8
- }
9
-
10
- export function formatStars(n: number | null | undefined): string | null {
11
- if (typeof n !== "number" || !Number.isFinite(n)) return null;
12
- return compact(n);
13
- }
14
-
15
- export function formatRelative(iso: string | null | undefined): string {
16
- if (!iso) return "—";
17
- const d = new Date(iso);
18
- if (Number.isNaN(d.valueOf())) return "—";
19
- const days = Math.floor((Date.now() - d.valueOf()) / (1000 * 60 * 60 * 24));
20
- if (days <= 0) return "today";
21
- if (days === 1) return "1d ago";
22
- if (days < 30) return `${days}d ago`;
23
- const months = Math.floor(days / 30);
24
- if (months === 1) return "1mo ago";
25
- if (months < 12) return `${months}mo ago`;
26
- const years = Math.floor(months / 12);
27
- if (years === 1) return "1y ago";
28
- return `${years}y ago`;
29
- }
30
-
31
- export function formatDate(iso: string | null | undefined): string {
32
- if (!iso) return "—";
33
- const d = new Date(iso);
34
- if (Number.isNaN(d.valueOf())) return iso;
35
- return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
36
- }
@@ -1,98 +0,0 @@
1
- /**
2
- * Curated lenses. The proxy lives here so the page can stay clean.
3
- * For a generic, headless version, see `@grove-dev/ui`.
4
- */
5
-
6
- export type LensId =
7
- | "all"
8
- | "new"
9
- | "hot"
10
- | "mature"
11
- | "good-to-learn"
12
- | "production-like"
13
- | "beginner-friendly"
14
- | "contribution-ready"
15
- | "launches"
16
- | "actively-developed"
17
- | "needs-maintainer";
18
-
19
- export interface LensDef {
20
- id: LensId;
21
- label: string;
22
- description?: string;
23
- toParams: () => Record<string, string | string[]>;
24
- }
25
-
26
- export const LENSES: LensDef[] = [
27
- { id: "all", label: "All apps", description: "Every app in the directory", toParams: () => ({}) },
28
- { id: "new", label: "Newly added", description: "Recently added to the directory", toParams: () => ({ label: "new" }) },
29
- { id: "hot", label: "Hot right now", description: "Apps gaining attention or recent activity", toParams: () => ({ label: "hot" }) },
30
- { id: "mature", label: "Mature", description: "Established long-running projects", toParams: () => ({ label: "mature" }) },
31
- { id: "production-like", label: "Production-grade", description: "Real apps, not toy projects", toParams: () => ({ lens: "production-like" }) },
32
- { id: "good-to-learn", label: "Good to learn", description: "Readable codebases with useful patterns", toParams: () => ({ lens: "good-to-learn" }) },
33
- { id: "beginner-friendly", label: "Beginner friendly", description: "Smaller, readable, easier to understand", toParams: () => ({ lens: "beginner-friendly" }) },
34
- { id: "contribution-ready", label: "Contribution ready", description: "Clear issues, active maintainers, license, contribution docs", toParams: () => ({ lens: "contribution-ready" }) },
35
- { id: "launches", label: "Launches", description: "Recently launched OSS apps seeking feedback", toParams: () => ({ lens: "launches" }) },
36
- { id: "actively-developed", label: "Actively developed", description: "Recent commits, releases, issue activity", toParams: () => ({ status: "active" }) },
37
- { id: "needs-maintainer", label: "Needs maintainer", description: "Useful apps that need help", toParams: () => ({ status: "stale,quiet" }) },
38
- ];
39
-
40
- export const PRIMARY_LENSES: LensId[] = [
41
- "all",
42
- "new",
43
- "hot",
44
- "mature",
45
- "production-like",
46
- "good-to-learn",
47
- ];
48
-
49
- export function lensById(id: string | null | undefined): LensDef | undefined {
50
- if (!id) return undefined;
51
- return LENSES.find((l) => l.id === id);
52
- }
53
-
54
- export function isLensActive(lensId: LensId, sp: URLSearchParams): boolean {
55
- if (lensId === "all") {
56
- return !lensFromSearchParams(sp) && !sp.get("label") && !sp.get("status");
57
- }
58
- const def = lensById(lensId);
59
- if (!def) return false;
60
- const target = def.toParams();
61
- for (const [k, v] of Object.entries(target)) {
62
- if (Array.isArray(v)) {
63
- if (sp.getAll(k).length !== v.length) return false;
64
- for (const item of v) if (!sp.getAll(k).includes(item)) return false;
65
- } else {
66
- if (sp.get(k) !== v) return false;
67
- }
68
- }
69
- return true;
70
- }
71
-
72
- export function isPrimaryLens(id: string | null | undefined): boolean {
73
- if (!id) return false;
74
- return PRIMARY_LENSES.includes(id as LensId);
75
- }
76
-
77
- export function lensToQuery(id: LensId | null | undefined, current: URLSearchParams): string {
78
- if (!id || id === "all") return "";
79
- const def = lensById(id);
80
- if (!def) return "";
81
- const next = new URLSearchParams(current);
82
- for (const [k, v] of Object.entries(def.toParams())) {
83
- if (Array.isArray(v)) {
84
- next.delete(k);
85
- for (const item of v) next.append(k, item);
86
- } else {
87
- next.set(k, v);
88
- }
89
- }
90
- return next.toString();
91
- }
92
-
93
- export function lensFromSearchParams(sp: URLSearchParams): LensId | null {
94
- const v = sp.get("lens");
95
- if (!v) return null;
96
- if (LENSES.some((l) => l.id === v)) return v as LensId;
97
- return null;
98
- }
@@ -1,12 +0,0 @@
1
- /** GitHub repo URL parsing helpers. */
2
- export function getOwnerAndRepoFromRepoUrl(repoUrl: string): { owner: string | null; repo: string | null } {
3
- if (!repoUrl || !repoUrl.includes("github.com/")) return { owner: null, repo: null };
4
- const m = repoUrl.match(/github\.com\/([^/]+)\/([^/?#]+)/);
5
- if (!m) return { owner: null, repo: null };
6
- return { owner: m[1], repo: m[2].replace(/\.git$/, "") };
7
- }
8
-
9
- export function getOwnerAvatarUrl(owner: string | null, size: number = 80): string | null {
10
- if (!owner) return null;
11
- return `https://avatars.githubusercontent.com/${owner}?v=4&s=${size}`;
12
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Score display helpers used across list and detail pages.
3
- * Generic version lives in `@grove-dev/ui` — this proxies the
4
- * `AppScores` shape defined in template `data/types.ts`.
5
- */
6
- import type { AppScores } from "../data/types";
7
-
8
- export function scoreTier(n: number): 0 | 1 | 2 | 3 | 4 {
9
- if (n < 20) return 0;
10
- if (n < 40) return 1;
11
- if (n < 60) return 2;
12
- if (n < 80) return 3;
13
- return 4;
14
- }
15
-
16
- export function scoreTierLabel(n: number): string {
17
- switch (scoreTier(n)) {
18
- case 0: return "Very low";
19
- case 1: return "Low";
20
- case 2: return "Medium";
21
- case 3: return "High";
22
- case 4: return "Very high";
23
- }
24
- }
25
-
26
- export function scoreLabel(n: number | undefined): string {
27
- if (typeof n !== "number") return "—";
28
- return Math.round(n).toString();
29
- }
30
-
31
- export const SCORE_DIMENSIONS: (keyof AppScores)[] = [
32
- "activity", "maturity", "learning", "contribution", "docs", "overall",
33
- ];
34
-
35
- export const SCORE_LABELS: Record<keyof AppScores, string> = {
36
- activity: "Activity",
37
- maturity: "Maturity",
38
- learning: "Learning",
39
- contribution: "Contribution",
40
- docs: "Docs",
41
- overall: "Overall",
42
- };
43
-
44
- export const SCORE_REASONING: Record<keyof AppScores, string> = {
45
- activity: "Recent commits, open and merged PRs, issue response time, release cadence.",
46
- maturity: "Repo age, stable structure, real-world production usage, contributor stability.",
47
- learning: "Codebase readability, useful architecture patterns, comment quality, examples.",
48
- contribution: "Open issues, recent merged PRs from new contributors, maintainer responsiveness, contribution guide.",
49
- docs: "README quality, architecture docs, contributing guide, examples, code comments.",
50
- overall: "Composite judgment — how likely this app is to satisfy a curious developer.",
51
- };