@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.
- package/LICENSE +21 -0
- 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/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 +306 -30
- package/src/components/MinimalAbout.astro +82 -0
- package/src/components/OriginalCollection.astro +116 -0
- package/src/components/RecordSection.astro +88 -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.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/grove.config.ts +36 -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 +193 -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 +263 -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
|
@@ -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
|
-
};
|