@grove-dev/astro 0.2.19 → 0.3.1
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/template-routes.test.d.ts +2 -0
- package/dist/template-routes.test.d.ts.map +1 -0
- package/dist/template-routes.test.js +63 -0
- package/dist/template-routes.test.js.map +1 -0
- package/dist/theme.test.js +19 -0
- package/dist/theme.test.js.map +1 -1
- package/package.json +4 -4
- package/src/components/CategoryGrid.astro +2 -11
- package/src/components/ContributorsGrid.astro +2 -11
- package/src/components/CurationGrid.astro +1 -1
- package/src/components/DecisionRow.astro +1 -1
- package/src/components/ExploreByCategory.astro +3 -3
- package/src/components/ExploreByStack.astro +3 -3
- package/src/components/FilterGroupMenu.astro +5 -5
- package/src/components/Hero.astro +8 -8
- package/src/components/Icon.astro +56 -53
- package/src/components/IndexRow.astro +5 -5
- package/src/components/ItemCard.astro +5 -16
- package/src/components/MinimalAbout.astro +2 -2
- package/src/components/OriginalCollection.astro +4 -4
- package/src/components/Pagination.astro +2 -2
- package/src/components/RecordSection.astro +1 -1
- package/src/components/RefinePanel.astro +1 -1
- package/src/components/SmartLensTabs.astro +3 -3
- package/src/components/StackGrid.astro +6 -15
- package/src/components/WhyThisExists.astro +3 -3
- package/src/layouts/Container.astro +4 -4
- package/src/layouts/Footer.astro +3 -3
- package/src/layouts/Header.astro +5 -5
- package/src/layouts/ThemeToggle.astro +1 -1
- package/src/styles.css +33 -339
- package/src/template-routes.test.ts +103 -0
- package/src/theme.test.ts +24 -0
- package/templates/default/.github/workflows/build.yml +1 -3
- package/templates/default/.github/workflows/cleanup-stale-records.yml +1 -3
- package/templates/default/.github/workflows/daily-refresh.yml +1 -3
- package/templates/default/.github/workflows/sync-contributors.yml +2 -4
- package/templates/default/.github/workflows/sync-github-metadata.yml +1 -3
- package/templates/default/.github/workflows/validate-data.yml +1 -3
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +9 -4
- package/templates/default/public/icons/stacks/rag.svg +7 -0
- package/templates/default/public/llms-full.txt +63 -186
- package/templates/default/public/llms.txt +6 -107
- package/templates/default/public/sitemap.xml +10 -10
- package/templates/default/src/data/records.ts +80 -2
- package/templates/default/src/pages/404.astro +5 -5
- package/templates/default/src/pages/[slug]/[recordSlug].astro +36 -38
- package/templates/default/src/pages/[slug]/index.astro +223 -19
- package/templates/default/src/pages/about.astro +17 -10
- package/templates/default/src/pages/apps/[recordSlug].astro +2 -1
- package/templates/default/src/pages/contributors.astro +8 -17
- package/templates/default/src/pages/index.astro +11 -3
- package/templates/default/src/pages/sitemap.xml.ts +3 -3
- package/templates/default/src/pages/submit.astro +28 -36
- package/templates/default/tsconfig.json +9 -0
- package/templates/default/scripts/build-llms.mjs +0 -123
- package/templates/default/scripts/build-records-json.mjs +0 -27
- package/templates/default/scripts/build-sitemap.mjs +0 -20
- package/templates/default/scripts/cleanup-stale-records.mjs +0 -20
- package/templates/default/scripts/enrich-github-metadata.mjs +0 -99
- package/templates/default/scripts/fetch-icons.mjs +0 -137
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +0 -86
- package/templates/default/scripts/parse-legacy-readme.mjs +0 -59
- package/templates/default/scripts/refresh-records-activity.mjs +0 -24
- package/templates/default/scripts/repair-license-format.mjs +0 -72
- package/templates/default/scripts/report-cleanup-candidates.mjs +0 -21
- package/templates/default/scripts/seed-from-github.mjs +0 -62
- package/templates/default/scripts/sync-contributors.mjs +0 -145
- package/templates/default/scripts/sync-github-metadata.mjs +0 -28
- package/templates/default/scripts/validate-records.mjs +0 -27
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
* logic. The URL stays the single source of truth for filter state.
|
|
21
21
|
*/
|
|
22
22
|
import siteConfig from "../../../data/generated/site-config.json";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
items,
|
|
25
|
+
itemLabel,
|
|
26
|
+
itemLabelPlural,
|
|
27
|
+
taxonomy,
|
|
28
|
+
taxonomyLabel,
|
|
29
|
+
} from "../../data/records";
|
|
24
30
|
import type { IndexFilters } from "@grove-dev/astro";
|
|
25
31
|
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
26
32
|
import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
|
|
@@ -41,19 +47,32 @@ import {
|
|
|
41
47
|
totalPages,
|
|
42
48
|
} from "@grove-dev/astro";
|
|
43
49
|
|
|
44
|
-
// This template ships with the `project-directory` blueprint, so the
|
|
45
|
-
// only directory slug it ever needs is `projects`. While it ships
|
|
46
|
-
// as the project directory, pre-generate the single path the build
|
|
47
|
-
// actually needs. Swapping in a different blueprint is a one-config
|
|
48
|
-
// change that flows through the static import at the top.
|
|
49
50
|
export function getStaticPaths() {
|
|
50
|
-
|
|
51
|
+
const routeSlug = siteConfig.blueprintConfig?.routeSlug ?? "projects";
|
|
52
|
+
return [{ params: { slug: routeSlug } }];
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
const slug = Astro.params.slug as string;
|
|
54
56
|
const total = items.length;
|
|
55
57
|
const filters = filtersFromSearchParams(Astro.url.searchParams);
|
|
56
|
-
|
|
58
|
+
function labelFacetsWithTaxonomy(facets: ReturnType<typeof buildFacets>) {
|
|
59
|
+
return {
|
|
60
|
+
...facets,
|
|
61
|
+
stacks: facets.stacks.map((option) => ({
|
|
62
|
+
...option,
|
|
63
|
+
label: taxonomyLabel("stacks", option.value),
|
|
64
|
+
})),
|
|
65
|
+
platforms: facets.platforms.map((option) => ({
|
|
66
|
+
...option,
|
|
67
|
+
label: taxonomyLabel("platforms", option.value),
|
|
68
|
+
})),
|
|
69
|
+
categories: facets.categories.map((option) => ({
|
|
70
|
+
...option,
|
|
71
|
+
label: taxonomyLabel("categories", option.value),
|
|
72
|
+
})),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const facets = labelFacetsWithTaxonomy(buildFacets(items));
|
|
57
76
|
const sort = effectiveSort(filters);
|
|
58
77
|
const requestedPage = effectivePage(filters);
|
|
59
78
|
const filtered = filterRecords(items, filters);
|
|
@@ -61,6 +80,7 @@ const sorted = applySort(filtered, sort);
|
|
|
61
80
|
const pages = totalPages(sorted.length);
|
|
62
81
|
const page = Math.min(requestedPage, pages);
|
|
63
82
|
const chips = activeFilterChips(filters);
|
|
83
|
+
const clientItemsJson = JSON.stringify(items).replace(/</g, "\\u003c");
|
|
64
84
|
|
|
65
85
|
const title = `Browse ${slug} — ${siteConfig.name}`;
|
|
66
86
|
const description = `A searchable directory of ${itemLabelPlural()} for ${siteConfig.name}.`;
|
|
@@ -73,7 +93,6 @@ function hrefForPage(target: number): string {
|
|
|
73
93
|
|
|
74
94
|
function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
75
95
|
const params = new URLSearchParams();
|
|
76
|
-
const next: IndexFilters = { ...filters, page: 1 };
|
|
77
96
|
const urlKey: Partial<Record<keyof IndexFilters, string>> = {
|
|
78
97
|
stacks: "stack",
|
|
79
98
|
platforms: "platform",
|
|
@@ -110,7 +129,7 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
110
129
|
submitLabel={`Submit ${itemLabel()}`}
|
|
111
130
|
>
|
|
112
131
|
<section class="border-b border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950">
|
|
113
|
-
<div class="
|
|
132
|
+
<div class="mx-auto w-full max-w-wide px-5 pt-10 sm:px-7 sm:pt-12">
|
|
114
133
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
115
134
|
{slug}
|
|
116
135
|
</span>
|
|
@@ -171,7 +190,7 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
171
190
|
<path d="M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z" />
|
|
172
191
|
</svg>
|
|
173
192
|
</label>
|
|
174
|
-
<button type="submit" class="
|
|
193
|
+
<button type="submit" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90">Search</button>
|
|
175
194
|
</form>
|
|
176
195
|
|
|
177
196
|
<div class="mt-4">
|
|
@@ -192,8 +211,8 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
192
211
|
</div>
|
|
193
212
|
</section>
|
|
194
213
|
|
|
195
|
-
<section class="
|
|
196
|
-
<div class="
|
|
214
|
+
<section class="py-10 sm:py-12">
|
|
215
|
+
<div class="mx-auto w-full max-w-wide px-5 sm:px-7">
|
|
197
216
|
<header class="mb-6">
|
|
198
217
|
<h2 class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
199
218
|
{`All ${itemLabelPlural()}`}
|
|
@@ -220,7 +239,7 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
220
239
|
{chips.map((chip) => (
|
|
221
240
|
<a
|
|
222
241
|
href={hrefForRemove(chip.key, chip.value)}
|
|
223
|
-
class="
|
|
242
|
+
class="inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground transition-colors hover:border-ink-400"
|
|
224
243
|
>
|
|
225
244
|
{chip.label} <span aria-hidden="true">x</span>
|
|
226
245
|
</a>
|
|
@@ -248,16 +267,16 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
248
267
|
<p class="m-0 text-sm font-medium text-ink-700 dark:text-ink-200">
|
|
249
268
|
{`No ${itemLabel()}s match these filters.`}
|
|
250
269
|
</p>
|
|
251
|
-
<a href={`/${slug}`} class="
|
|
270
|
+
<a href={`/${slug}`} class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 border-border bg-secondary text-foreground hover:bg-accent mt-4">Clear filters</a>
|
|
252
271
|
</div>
|
|
253
|
-
<nav class="mt-8 flex flex-wrap items-center justify-center gap-1.5" aria-label="Pagination">
|
|
272
|
+
<nav id="results-pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" aria-label="Pagination">
|
|
254
273
|
<Pagination current={page} total={pages} hrefFor={hrefForPage} />
|
|
255
274
|
</nav>
|
|
256
275
|
</div>
|
|
257
276
|
</section>
|
|
258
277
|
|
|
259
278
|
<section class="border-t border-ink-200 dark:border-ink-800">
|
|
260
|
-
<div class="
|
|
279
|
+
<div class="mx-auto w-full max-w-wide px-5 sm:px-7">
|
|
261
280
|
<div class="flex flex-col items-center gap-3 py-12 text-center">
|
|
262
281
|
<h2 class="m-0 text-xl font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
263
282
|
{`Know a real ${itemLabel()} that belongs here?`}
|
|
@@ -266,8 +285,8 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
266
285
|
{`Open a pull request with a YAML file. We review every submission against the bar — no marketing, no tutorials.`}
|
|
267
286
|
</p>
|
|
268
287
|
<a
|
|
269
|
-
href=
|
|
270
|
-
class="
|
|
288
|
+
href="/submit"
|
|
289
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90 mt-2"
|
|
271
290
|
>
|
|
272
291
|
{`Submit ${itemLabel()}`}
|
|
273
292
|
</a>
|
|
@@ -275,4 +294,189 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
|
|
|
275
294
|
</div>
|
|
276
295
|
</div>
|
|
277
296
|
</section>
|
|
297
|
+
|
|
298
|
+
<script is:inline id="grove-index-data" type="application/json" set:html={clientItemsJson}></script>
|
|
299
|
+
<script
|
|
300
|
+
is:inline
|
|
301
|
+
define:vars={{
|
|
302
|
+
slug,
|
|
303
|
+
singular: itemLabel(),
|
|
304
|
+
plural: itemLabelPlural(),
|
|
305
|
+
taxonomy,
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
(() => {
|
|
309
|
+
const PAGE_SIZE = 20;
|
|
310
|
+
const source = document.querySelector("#grove-index-data");
|
|
311
|
+
const list = document.querySelector("#results-list");
|
|
312
|
+
if (!source || !list) return;
|
|
313
|
+
|
|
314
|
+
const items = JSON.parse(source.textContent || "[]");
|
|
315
|
+
const nodes = new Map(
|
|
316
|
+
[...list.querySelectorAll("[data-record-slug]")].map((node) => [
|
|
317
|
+
node.dataset.recordSlug,
|
|
318
|
+
node,
|
|
319
|
+
]),
|
|
320
|
+
);
|
|
321
|
+
const params = new URLSearchParams(location.search);
|
|
322
|
+
const values = (key) => params.getAll(key).filter(Boolean);
|
|
323
|
+
const q = (params.get("q") || "").trim().toLowerCase();
|
|
324
|
+
const stacks = values("stack");
|
|
325
|
+
const platforms = values("platform");
|
|
326
|
+
const categories = values("category");
|
|
327
|
+
const labels = values("label");
|
|
328
|
+
const licenses = values("license");
|
|
329
|
+
const statuses = values("status").flatMap((value) => value.split(",")).filter(Boolean);
|
|
330
|
+
const lens = params.get("lens") || "";
|
|
331
|
+
const sort = params.get("sort") || "recently-updated";
|
|
332
|
+
const requestedPage = Math.max(1, Number.parseInt(params.get("page") || "1", 10) || 1);
|
|
333
|
+
|
|
334
|
+
const nameOf = (record) => record.kind === "resource" ? record.title : record.name;
|
|
335
|
+
const projectValues = (record) => record.kind === "project"
|
|
336
|
+
? [record.stack, ...(record.stacks || [])].filter(Boolean)
|
|
337
|
+
: [];
|
|
338
|
+
const matchesAny = (actual, expected) =>
|
|
339
|
+
expected.length === 0 || actual.some((value) => expected.includes(value));
|
|
340
|
+
const matches = (record) => {
|
|
341
|
+
if (q) {
|
|
342
|
+
const repo = record.kind === "project"
|
|
343
|
+
? record.repoUrl || record.links?.github || ""
|
|
344
|
+
: record.links?.github || "";
|
|
345
|
+
const owner = /github\.com\/([^/]+)\//.exec(repo)?.[1] || "";
|
|
346
|
+
const projectFields = record.kind === "project"
|
|
347
|
+
? [
|
|
348
|
+
record.stack,
|
|
349
|
+
...(record.stacks || []),
|
|
350
|
+
...(record.platforms || []),
|
|
351
|
+
record.projectType,
|
|
352
|
+
...(record.bestFor || []),
|
|
353
|
+
...(record.whyListed || []),
|
|
354
|
+
record.github?.license,
|
|
355
|
+
record.health?.status,
|
|
356
|
+
]
|
|
357
|
+
: [];
|
|
358
|
+
const haystack = [
|
|
359
|
+
nameOf(record),
|
|
360
|
+
owner,
|
|
361
|
+
record.description,
|
|
362
|
+
record.category,
|
|
363
|
+
...(record.tags || []),
|
|
364
|
+
...projectFields,
|
|
365
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
366
|
+
if (!haystack.includes(q)) return false;
|
|
367
|
+
}
|
|
368
|
+
if (!matchesAny(projectValues(record), stacks)) return false;
|
|
369
|
+
if (!matchesAny(record.kind === "project" ? record.platforms || [] : [], platforms)) return false;
|
|
370
|
+
if (categories.length && !categories.includes(record.category)) return false;
|
|
371
|
+
if (!matchesAny(record.curation?.labels || [], labels)) return false;
|
|
372
|
+
if (licenses.length && !licenses.includes(record.kind === "project" ? record.github?.license : "")) return false;
|
|
373
|
+
if (statuses.length && !statuses.includes(record.kind === "project" ? record.health?.status : "")) return false;
|
|
374
|
+
if (lens && !(record.curation?.lenses || []).includes(lens)) return false;
|
|
375
|
+
return true;
|
|
376
|
+
};
|
|
377
|
+
const time = (value) => value ? new Date(value).valueOf() || 0 : 0;
|
|
378
|
+
const stars = (record) => record.kind === "project" ? record.github?.stars || 0 : 0;
|
|
379
|
+
const compare = {
|
|
380
|
+
"most-starred": (a, b) => stars(b) - stars(a),
|
|
381
|
+
"recently-updated": (a, b) =>
|
|
382
|
+
time(b.kind === "project" ? b.github?.pushedAt : b.publishedAt) -
|
|
383
|
+
time(a.kind === "project" ? a.github?.pushedAt : a.publishedAt),
|
|
384
|
+
"recently-added": (a, b) =>
|
|
385
|
+
time(b.curation?.reviewedAt) - time(a.curation?.reviewedAt),
|
|
386
|
+
"best-overall": (a, b) =>
|
|
387
|
+
Number(Boolean(b.curation?.reviewed)) - Number(Boolean(a.curation?.reviewed)) ||
|
|
388
|
+
stars(b) - stars(a),
|
|
389
|
+
"alphabetical": (a, b) => String(nameOf(a)).localeCompare(String(nameOf(b))),
|
|
390
|
+
}[sort];
|
|
391
|
+
|
|
392
|
+
function applyClientFilters() {
|
|
393
|
+
const filtered = items.filter(matches);
|
|
394
|
+
if (compare) filtered.sort(compare);
|
|
395
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
|
396
|
+
const page = Math.min(requestedPage, pageCount);
|
|
397
|
+
const visible = new Set(
|
|
398
|
+
filtered
|
|
399
|
+
.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
|
|
400
|
+
.map((record) => record.slug),
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
for (const record of filtered) {
|
|
404
|
+
const node = nodes.get(record.slug);
|
|
405
|
+
if (node) list.append(node);
|
|
406
|
+
}
|
|
407
|
+
for (const [recordSlug, node] of nodes) {
|
|
408
|
+
node.hidden = !visible.has(recordSlug);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const count = document.querySelector("#results-count");
|
|
412
|
+
const pageLabel = document.querySelector("#results-page");
|
|
413
|
+
const empty = document.querySelector("#empty-state");
|
|
414
|
+
if (count) count.textContent = `${filtered.length} ${filtered.length === 1 ? singular : plural}`;
|
|
415
|
+
if (pageLabel) pageLabel.textContent = pageCount > 1 ? ` · page ${page} of ${pageCount}` : "";
|
|
416
|
+
if (empty) empty.classList.toggle("hidden", filtered.length > 0);
|
|
417
|
+
|
|
418
|
+
const pagination = document.querySelector("#results-pagination");
|
|
419
|
+
if (pagination) {
|
|
420
|
+
pagination.innerHTML = "";
|
|
421
|
+
if (pageCount > 1) {
|
|
422
|
+
for (let target = 1; target <= pageCount; target += 1) {
|
|
423
|
+
const next = new URLSearchParams(params);
|
|
424
|
+
if (target === 1) next.delete("page");
|
|
425
|
+
else next.set("page", String(target));
|
|
426
|
+
const link = document.createElement("a");
|
|
427
|
+
link.href = `/${slug}${next.size ? `?${next}` : ""}`;
|
|
428
|
+
link.textContent = String(target);
|
|
429
|
+
link.setAttribute("aria-label", `Page ${target}`);
|
|
430
|
+
if (target === page) link.setAttribute("aria-current", "page");
|
|
431
|
+
link.className = "inline-flex h-8 min-w-8 items-center justify-center rounded-md border border-border px-2 text-sm";
|
|
432
|
+
pagination.append(link);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const searchInput = document.querySelector("#search-input");
|
|
439
|
+
if (searchInput) searchInput.value = params.get("q") || "";
|
|
440
|
+
document.querySelectorAll('select[name="sort"]').forEach((select) => {
|
|
441
|
+
select.value = sort;
|
|
442
|
+
});
|
|
443
|
+
const filterKeys = {
|
|
444
|
+
stacks: "stack",
|
|
445
|
+
platforms: "platform",
|
|
446
|
+
categories: "category",
|
|
447
|
+
licenses: "license",
|
|
448
|
+
};
|
|
449
|
+
const filterLabels = {
|
|
450
|
+
stacks: "Stack",
|
|
451
|
+
platforms: "Platform",
|
|
452
|
+
categories: "Category",
|
|
453
|
+
licenses: "License",
|
|
454
|
+
};
|
|
455
|
+
const taxonomyKinds = {
|
|
456
|
+
stacks: "stacks",
|
|
457
|
+
platforms: "platforms",
|
|
458
|
+
categories: "categories",
|
|
459
|
+
};
|
|
460
|
+
const taxonomyName = (groupKey, value) =>
|
|
461
|
+
(taxonomy[taxonomyKinds[groupKey]] || []).find((entry) => entry.id === value)?.name ??
|
|
462
|
+
value;
|
|
463
|
+
document.querySelectorAll(".grove-filter").forEach((group) => {
|
|
464
|
+
const selected = values(filterKeys[group.dataset.filterGroupKey]);
|
|
465
|
+
group.querySelectorAll(".grove-filter-input").forEach((input) => {
|
|
466
|
+
input.checked = selected.includes(input.dataset.filterValue);
|
|
467
|
+
});
|
|
468
|
+
const trigger = group.querySelector(".grove-filter-trigger span");
|
|
469
|
+
if (trigger && selected.length) {
|
|
470
|
+
const label = filterLabels[group.dataset.filterGroupKey];
|
|
471
|
+
trigger.textContent = `${label}: ${
|
|
472
|
+
selected.length === 1
|
|
473
|
+
? taxonomyName(group.dataset.filterGroupKey, selected[0])
|
|
474
|
+
: `${selected.length} selected`
|
|
475
|
+
}`;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
applyClientFilters();
|
|
480
|
+
})();
|
|
481
|
+
</script>
|
|
278
482
|
</BaseLayout>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - New: "A growing community knowledge site. A curated, health-
|
|
14
14
|
* aware developer directory…"
|
|
15
15
|
*
|
|
16
|
-
* The card
|
|
16
|
+
* The Tailwind card and button utilities match the
|
|
17
17
|
* rest of the framework, and the data fields it references
|
|
18
18
|
* (`siteConfig.tagline`, `siteConfig.repoUrl`) all flow from the
|
|
19
19
|
* site config — no hardcoded brand or copy.
|
|
@@ -21,13 +21,14 @@
|
|
|
21
21
|
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
22
22
|
import Container from "@grove-dev/astro/layouts/Container.astro";
|
|
23
23
|
import siteConfig from "../../data/generated/site-config.json";
|
|
24
|
-
import { indexSlug, itemLabel } from "../data/records";
|
|
24
|
+
import { getPageContentHtml, indexSlug, itemLabel } from "../data/records";
|
|
25
25
|
|
|
26
26
|
const githubUrl = siteConfig.repoUrl;
|
|
27
27
|
const title = `About — ${siteConfig.name}`;
|
|
28
28
|
const description = `${siteConfig.tagline} Built with Grove.`;
|
|
29
29
|
const slug = indexSlug();
|
|
30
30
|
const item = itemLabel();
|
|
31
|
+
const pageContentHtml = getPageContentHtml("about");
|
|
31
32
|
---
|
|
32
33
|
|
|
33
34
|
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
@@ -39,6 +40,10 @@ const item = itemLabel();
|
|
|
39
40
|
<span class="text-ink-700 dark:text-ink-300">About</span>
|
|
40
41
|
</nav>
|
|
41
42
|
|
|
43
|
+
{pageContentHtml ? (
|
|
44
|
+
<section class="grove-prose mt-8" set:html={pageContentHtml} />
|
|
45
|
+
) : (
|
|
46
|
+
<>
|
|
42
47
|
<header class="mt-5">
|
|
43
48
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
44
49
|
About
|
|
@@ -57,7 +62,7 @@ const item = itemLabel();
|
|
|
57
62
|
</p>
|
|
58
63
|
</header>
|
|
59
64
|
|
|
60
|
-
<section class="
|
|
65
|
+
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-8 p-5 sm:p-6">
|
|
61
66
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
62
67
|
Why
|
|
63
68
|
</span>
|
|
@@ -78,7 +83,7 @@ const item = itemLabel();
|
|
|
78
83
|
</p>
|
|
79
84
|
</section>
|
|
80
85
|
|
|
81
|
-
<section class="
|
|
86
|
+
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
82
87
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
83
88
|
Why real {item}s
|
|
84
89
|
</span>
|
|
@@ -95,7 +100,7 @@ const item = itemLabel();
|
|
|
95
100
|
</p>
|
|
96
101
|
</section>
|
|
97
102
|
|
|
98
|
-
<section class="
|
|
103
|
+
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
99
104
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
100
105
|
What you get
|
|
101
106
|
</span>
|
|
@@ -130,7 +135,7 @@ const item = itemLabel();
|
|
|
130
135
|
</ul>
|
|
131
136
|
</section>
|
|
132
137
|
|
|
133
|
-
<section class="
|
|
138
|
+
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
134
139
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
135
140
|
Generic by design
|
|
136
141
|
</span>
|
|
@@ -145,7 +150,7 @@ const item = itemLabel();
|
|
|
145
150
|
</p>
|
|
146
151
|
</section>
|
|
147
152
|
|
|
148
|
-
<section class="
|
|
153
|
+
<section class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors mt-4 p-5 sm:p-6">
|
|
149
154
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
150
155
|
Free to fork, free to extend
|
|
151
156
|
</span>
|
|
@@ -164,19 +169,21 @@ const item = itemLabel();
|
|
|
164
169
|
</section>
|
|
165
170
|
|
|
166
171
|
<section class="mt-8 flex flex-wrap items-center gap-3">
|
|
167
|
-
<a href={`/${slug}`} class="
|
|
172
|
+
<a href={`/${slug}`} class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-primary text-primary-foreground hover:bg-primary/90">Browse the directory</a>
|
|
168
173
|
{githubUrl && (
|
|
169
174
|
<a
|
|
170
175
|
href={githubUrl}
|
|
171
176
|
target="_blank"
|
|
172
177
|
rel="noopener noreferrer"
|
|
173
|
-
class="
|
|
178
|
+
class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 border-border bg-secondary text-foreground hover:bg-accent"
|
|
174
179
|
>
|
|
175
180
|
View on GitHub
|
|
176
181
|
</a>
|
|
177
182
|
)}
|
|
178
|
-
<a href="/submit" class="
|
|
183
|
+
<a href="/submit" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent px-3 text-sm font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35 bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground">Submit a {item} →</a>
|
|
179
184
|
</section>
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
180
187
|
</article>
|
|
181
188
|
</Container>
|
|
182
189
|
</BaseLayout>
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* this page is a pure redirect: any `/apps/<slug>` request is
|
|
11
11
|
* sent to the blueprint-canonical path.
|
|
12
12
|
*/
|
|
13
|
-
import { indexSlug, fullRecords,
|
|
13
|
+
import { indexSlug, fullRecords, recordBySlug } from "../../data/records";
|
|
14
14
|
|
|
15
15
|
export async function getStaticPaths() {
|
|
16
|
+
if (indexSlug() === "apps") return [];
|
|
16
17
|
return fullRecords.map((r) => ({ params: { recordSlug: r.slug } }));
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -82,8 +82,8 @@ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/co
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
84
|
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
85
|
-
<section class="
|
|
86
|
-
<div class="
|
|
85
|
+
<section class="py-12 sm:py-16">
|
|
86
|
+
<div class="mx-auto w-full max-w-wide px-5 py-12 sm:px-7 sm:py-16">
|
|
87
87
|
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
88
88
|
Contributors
|
|
89
89
|
</span>
|
|
@@ -112,9 +112,9 @@ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/co
|
|
|
112
112
|
</section>
|
|
113
113
|
|
|
114
114
|
{sorted.length === 0 ? (
|
|
115
|
-
<section class="
|
|
116
|
-
<div class="
|
|
117
|
-
<div class="
|
|
115
|
+
<section class="py-12 sm:py-16">
|
|
116
|
+
<div class="mx-auto w-full max-w-container px-5 pb-16 sm:px-7">
|
|
117
|
+
<div class="rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col items-center gap-3 p-8 text-center">
|
|
118
118
|
<p class="m-0 text-sm text-ink-500 dark:text-ink-400">
|
|
119
119
|
No contributors synced yet. Once the first sync runs, every
|
|
120
120
|
contributor avatar lands here.
|
|
@@ -123,8 +123,8 @@ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/co
|
|
|
123
123
|
</div>
|
|
124
124
|
</section>
|
|
125
125
|
) : (
|
|
126
|
-
<section class="
|
|
127
|
-
<div class="
|
|
126
|
+
<section class="py-12 sm:py-16" aria-label="Contributors list">
|
|
127
|
+
<div class="mx-auto w-full max-w-wide px-5 pb-16 sm:px-7">
|
|
128
128
|
<ul
|
|
129
129
|
class="m-0 grid list-none grid-cols-2 gap-3 p-0 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6"
|
|
130
130
|
aria-label="Contributors"
|
|
@@ -140,7 +140,7 @@ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/co
|
|
|
140
140
|
href={href}
|
|
141
141
|
target="_blank"
|
|
142
142
|
rel="noopener noreferrer"
|
|
143
|
-
class="group
|
|
143
|
+
class="group rounded-[calc(var(--radius)+0.25rem)] border-0 bg-muted shadow-none transition-colors flex flex-col items-center gap-2 p-3 text-center no-underline hover:border-ink-300 dark:hover:border-ink-700"
|
|
144
144
|
data-event="contributor_click"
|
|
145
145
|
data-event-source="contributors_page"
|
|
146
146
|
>
|
|
@@ -173,12 +173,3 @@ const contributorsGraphHref = repoUrl ? `${repoUrl.replace(/\/$/, "")}/graphs/co
|
|
|
173
173
|
</section>
|
|
174
174
|
)}
|
|
175
175
|
</BaseLayout>
|
|
176
|
-
|
|
177
|
-
<style>
|
|
178
|
-
.line-clamp-1 {
|
|
179
|
-
display: -webkit-box;
|
|
180
|
-
-webkit-line-clamp: 1;
|
|
181
|
-
-webkit-box-orient: vertical;
|
|
182
|
-
overflow: hidden;
|
|
183
|
-
}
|
|
184
|
-
</style>
|
|
@@ -20,7 +20,7 @@ import CategoryGrid from "@grove-dev/astro/components/CategoryGrid.astro";
|
|
|
20
20
|
import ContributorsGrid from "@grove-dev/astro/components/ContributorsGrid.astro";
|
|
21
21
|
import OriginalCollection from "@grove-dev/astro/components/OriginalCollection.astro";
|
|
22
22
|
|
|
23
|
-
import { items,
|
|
23
|
+
import { items, fullItems, taxonomyLabel } from "../data/records";
|
|
24
24
|
import { existsSync, readFileSync } from "node:fs";
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { dirname, resolve } from "node:path";
|
|
@@ -87,10 +87,18 @@ for (const r of fullItems) {
|
|
|
87
87
|
const stacks = [...stackMap.entries()]
|
|
88
88
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
89
89
|
.slice(0, 12)
|
|
90
|
-
.map(([name, count]) => ({
|
|
90
|
+
.map(([name, count]) => ({
|
|
91
|
+
name: taxonomyLabel("stacks", name),
|
|
92
|
+
slug: name,
|
|
93
|
+
count,
|
|
94
|
+
}));
|
|
91
95
|
const categories = [...catMap.entries()]
|
|
92
96
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
93
|
-
.map(([name, count]) => ({
|
|
97
|
+
.map(([name, count]) => ({
|
|
98
|
+
name: taxonomyLabel("categories", name),
|
|
99
|
+
slug: name,
|
|
100
|
+
count,
|
|
101
|
+
}));
|
|
94
102
|
|
|
95
103
|
// Contributors (best-effort from data/generated/contributors.json)
|
|
96
104
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,8 +30,8 @@ function absoluteUrl(siteUrl: string, path: string): string {
|
|
|
30
30
|
return `${base}${tail}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function buildEntries(siteUrl: string
|
|
34
|
-
const slug = indexSlug(
|
|
33
|
+
function buildEntries(siteUrl: string): SitemapEntry[] {
|
|
34
|
+
const slug = indexSlug();
|
|
35
35
|
const now = new Date().toISOString().slice(0, 10);
|
|
36
36
|
|
|
37
37
|
const top: SitemapEntry[] = [
|
|
@@ -78,7 +78,7 @@ function renderXml(entries: SitemapEntry[]): string {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export const GET: APIRoute = () => {
|
|
81
|
-
const xml = renderXml(buildEntries(siteConfig.siteUrl ?? ""
|
|
81
|
+
const xml = renderXml(buildEntries(siteConfig.siteUrl ?? ""));
|
|
82
82
|
return new Response(xml, {
|
|
83
83
|
status: 200,
|
|
84
84
|
headers: {
|