@grove-dev/astro 0.2.6 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/display.d.ts +11 -17
- package/dist/lib/display.d.ts.map +1 -1
- package/dist/lib/display.js +43 -43
- package/dist/lib/display.js.map +1 -1
- package/dist/lib/format.d.ts +6 -7
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +11 -12
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/lenses.d.ts +35 -73
- package/dist/lib/lenses.d.ts.map +1 -1
- package/dist/lib/lenses.js +144 -126
- package/dist/lib/lenses.js.map +1 -1
- package/dist/lib/repo.d.ts +11 -22
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +12 -25
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/scores.d.ts +15 -16
- package/dist/lib/scores.d.ts.map +1 -1
- package/dist/lib/scores.js +4 -11
- package/dist/lib/scores.js.map +1 -1
- package/dist/lib/search.d.ts +47 -129
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +195 -180
- package/dist/lib/search.js.map +1 -1
- package/dist/lib/taxonomy-counts.d.ts +7 -21
- package/dist/lib/taxonomy-counts.d.ts.map +1 -1
- package/dist/lib/taxonomy-counts.js +12 -31
- package/dist/lib/taxonomy-counts.js.map +1 -1
- package/package.json +4 -4
- package/src/components/AppsIndexRow.astro +55 -184
- package/src/components/AppsPagination.astro +45 -108
- package/src/components/CategoryGrid.astro +43 -31
- package/src/components/ContributorsGrid.astro +3 -3
- package/src/components/CurationGrid.astro +41 -0
- package/src/components/ExploreByStack.astro +6 -0
- package/src/components/FilterGroupMenu.astro +49 -0
- package/src/components/FilterOptions.astro +44 -0
- package/src/components/Hero.astro +198 -147
- package/src/components/Icon.astro +10 -3
- package/src/components/ItemCard.astro +266 -248
- package/src/components/RecordSection.astro +68 -33
- package/src/components/RefinePanel.astro +115 -141
- package/src/components/StackGrid.astro +32 -24
- package/src/components/WhyThisExists.astro +32 -32
- package/src/layouts/BaseLayout.astro +104 -49
- package/src/layouts/Container.astro +5 -44
- package/src/layouts/Footer.astro +31 -20
- package/src/layouts/Header.astro +40 -19
- package/src/layouts/SectionHeader.astro +27 -43
- package/src/layouts/Seo.astro +38 -56
- package/src/layouts/ThemeToggle.astro +35 -48
- package/src/lib/display.ts +46 -53
- package/src/lib/format.ts +11 -12
- package/src/lib/lenses.ts +151 -172
- package/src/lib/repo.ts +16 -28
- package/src/lib/scores.ts +15 -19
- package/src/lib/search.ts +222 -290
- package/src/lib/taxonomy-counts.ts +12 -46
- package/src/styles.css +207 -1
- package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
- package/templates/default/.github/pull_request_template.md +27 -0
- package/templates/default/.github/workflows/build.yml +55 -0
- package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
- package/templates/default/.github/workflows/daily-refresh.yml +53 -0
- package/templates/default/.github/workflows/sync-contributors.yml +53 -0
- package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
- package/templates/default/.github/workflows/validate-data.yml +35 -0
- package/templates/default/data/records/coolify.yml +46 -0
- package/templates/default/data/records/gitea.yml +46 -0
- package/templates/default/data/records/hoppscotch.yml +47 -0
- package/templates/default/data/records/inventree.yml +47 -0
- package/templates/default/data/records/label-studio.yml +46 -0
- package/templates/default/data/records/logseq.yml +48 -0
- package/templates/default/data/records/private-gpt.yml +47 -0
- package/templates/default/data/records/wakapi.yml +44 -0
- package/templates/default/data/taxonomy/categories.yml +47 -0
- package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
- package/templates/default/data/taxonomy/platforms.yml +27 -0
- package/templates/default/data/taxonomy/stacks.yml +41 -0
- package/templates/default/grove.config.ts +1 -1
- package/templates/default/package.json +5 -4
- package/templates/default/public/icons/stacks/bun.svg +1 -0
- package/templates/default/public/icons/stacks/clojure.svg +1 -0
- package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
- package/templates/default/public/icons/stacks/deno.svg +1 -0
- package/templates/default/public/icons/stacks/django.svg +1 -0
- package/templates/default/public/icons/stacks/docker.svg +1 -0
- package/templates/default/public/icons/stacks/go.svg +1 -0
- package/templates/default/public/icons/stacks/llm.svg +1 -0
- package/templates/default/public/icons/stacks/node.js.svg +1 -0
- package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
- package/templates/default/public/icons/stacks/vue.svg +1 -0
- package/templates/default/public/llms-full.txt +197 -5
- package/templates/default/public/llms.txt +107 -6
- package/templates/default/public/sitemap.xml +63 -0
- package/templates/default/scripts/build-llms.mjs +123 -0
- package/templates/default/scripts/build-records-json.mjs +27 -0
- package/templates/default/scripts/build-sitemap.mjs +20 -0
- package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
- package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
- package/templates/default/scripts/fetch-icons.mjs +137 -0
- package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
- package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
- package/templates/default/scripts/refresh-records-activity.mjs +24 -0
- package/templates/default/scripts/repair-license-format.mjs +72 -0
- package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
- package/templates/default/scripts/seed-from-github.mjs +62 -0
- package/templates/default/scripts/sync-contributors.mjs +145 -0
- package/templates/default/scripts/sync-github-metadata.mjs +28 -0
- package/templates/default/scripts/validate-records.mjs +27 -0
- package/templates/default/src/data/records.ts +134 -51
- package/templates/default/src/pages/404.astro +64 -0
- package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
- package/templates/default/src/pages/[slug]/index.astro +476 -0
- package/templates/default/src/pages/about.astro +165 -82
- package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
- package/templates/default/src/pages/apps/index.astro +146 -0
- package/templates/default/src/pages/contributors.astro +184 -0
- package/templates/default/src/pages/index.astro +119 -162
- package/templates/default/src/pages/submit.astro +276 -114
- package/templates/default/wrangler.jsonc +28 -0
- package/LICENSE +0 -21
- package/templates/default/data/generated/apps.json +0 -1
- package/templates/default/data/generated/records.full.json +0 -8
- package/templates/default/data/generated/records.index.json +0 -7
- package/templates/default/data/generated/records.json +0 -8
- package/templates/default/data/generated/repo-stats.json +0 -1
- package/templates/default/data/generated/site-config.json +0 -31
- package/templates/default/src/components/layout/BaseLayout.astro +0 -47
- package/templates/default/src/components/layout/Container.astro +0 -18
- package/templates/default/src/components/layout/Footer.astro +0 -27
- package/templates/default/src/components/layout/Header.astro +0 -43
- package/templates/default/src/lib/markdown.ts +0 -171
- package/templates/default/src/pages/projects/index.astro +0 -193
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* List page — the discovery / browse page.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the openapps list structure:
|
|
6
|
+
* - SmartLensTabs (All / Trending / Recently added / Established / Featured)
|
|
7
|
+
* - RefinePanel (Stack / Platform / Category / License / Sort)
|
|
8
|
+
* - ItemSection rows (full record cards via RecordSection)
|
|
9
|
+
* - ExploreByCategory / ExploreByStack (compact pill lists)
|
|
10
|
+
*
|
|
11
|
+
* Generic: the URL slug (e.g. `/projects/`, `/resources/`,
|
|
12
|
+
* `/entities/`) is derived from the blueprint via `getStaticPaths`,
|
|
13
|
+
* so the same page works for every blueprint without per-blueprint
|
|
14
|
+
* forks.
|
|
15
|
+
*/
|
|
16
|
+
import siteConfig from "../../../data/generated/site-config.json";
|
|
17
|
+
import { items, itemLabel, itemLabelPlural } from "../../data/records";
|
|
18
|
+
import type { AppsFilters } from "@grove-dev/astro";
|
|
19
|
+
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
20
|
+
import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
|
|
21
|
+
import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
|
|
22
|
+
import AppsIndexRow from "@grove-dev/astro/components/AppsIndexRow.astro";
|
|
23
|
+
import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
|
|
24
|
+
import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
|
|
25
|
+
import {
|
|
26
|
+
activeFilterChips,
|
|
27
|
+
applySort,
|
|
28
|
+
buildFacets,
|
|
29
|
+
countByCategory,
|
|
30
|
+
countByStack,
|
|
31
|
+
effectivePage,
|
|
32
|
+
effectiveSort,
|
|
33
|
+
filterApps,
|
|
34
|
+
filtersFromSearchParams,
|
|
35
|
+
paginate,
|
|
36
|
+
searchParamsFromFilters,
|
|
37
|
+
sortDisplay,
|
|
38
|
+
totalPages,
|
|
39
|
+
} from "@grove-dev/astro";
|
|
40
|
+
|
|
41
|
+
export async function getStaticPaths() {
|
|
42
|
+
let activeSlug = "projects";
|
|
43
|
+
try {
|
|
44
|
+
const raw = await import("../../../data/generated/site-config.json");
|
|
45
|
+
activeSlug = raw.default?.blueprintConfig?.routeSlug ?? "projects";
|
|
46
|
+
} catch {
|
|
47
|
+
/* not present */
|
|
48
|
+
}
|
|
49
|
+
const all = ["projects", "resources", "entities"];
|
|
50
|
+
if (!all.includes(activeSlug)) all.push(activeSlug);
|
|
51
|
+
return all.map((slug) => ({ params: { slug } }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const slug = Astro.params.slug as string;
|
|
55
|
+
const total = items.length;
|
|
56
|
+
const filters = filtersFromSearchParams(Astro.url.searchParams);
|
|
57
|
+
const facets = buildFacets(items);
|
|
58
|
+
const sort = effectiveSort(filters);
|
|
59
|
+
const requestedPage = effectivePage(filters);
|
|
60
|
+
const filtered = filterApps(items, filters);
|
|
61
|
+
const sorted = applySort(filtered, sort);
|
|
62
|
+
const pages = totalPages(sorted.length);
|
|
63
|
+
const page = Math.min(requestedPage, pages);
|
|
64
|
+
const visibleItems = paginate(sorted, page);
|
|
65
|
+
const chips = activeFilterChips(filters);
|
|
66
|
+
const clientItems = items.map((item) => ({
|
|
67
|
+
slug: item.slug,
|
|
68
|
+
name: item.kind === "resource" ? item.title : item.name,
|
|
69
|
+
description: item.description,
|
|
70
|
+
category: item.category,
|
|
71
|
+
tags: item.tags,
|
|
72
|
+
labels: item.curation?.labels ?? [],
|
|
73
|
+
lenses: item.curation?.lenses ?? [],
|
|
74
|
+
reviewed: Boolean(item.curation?.reviewed),
|
|
75
|
+
addedAt: item.curation?.reviewedAt ?? "",
|
|
76
|
+
stacks: item.kind === "project" ? [item.stack, ...item.stacks].filter(Boolean) : [],
|
|
77
|
+
platforms: item.kind === "project" ? item.platforms : [],
|
|
78
|
+
license: item.kind === "project" ? item.github?.license ?? "" : "",
|
|
79
|
+
status: item.kind === "project" ? item.health?.status ?? "" : "",
|
|
80
|
+
stars: item.kind === "project" ? item.github?.stars ?? 0 : 0,
|
|
81
|
+
updatedAt:
|
|
82
|
+
item.kind === "project"
|
|
83
|
+
? item.github?.pushedAt ?? ""
|
|
84
|
+
: item.kind === "resource"
|
|
85
|
+
? item.publishedAt ?? ""
|
|
86
|
+
: "",
|
|
87
|
+
extra:
|
|
88
|
+
item.kind === "project"
|
|
89
|
+
? [item.repoUrl, item.projectType, item.difficulty, item.codebaseSize, ...item.bestFor, ...item.whyListed]
|
|
90
|
+
: item.kind === "resource"
|
|
91
|
+
? [item.type, item.topic, item.author ?? ""]
|
|
92
|
+
: [item.type, item.location ?? "", item.parent ?? ""],
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
const title = `Browse ${slug} — ${siteConfig.name}`;
|
|
96
|
+
const description = `A searchable directory of ${itemLabelPlural()} for ${siteConfig.name}.`;
|
|
97
|
+
|
|
98
|
+
// Compute the top categories and stacks for the explore pills
|
|
99
|
+
const categoryEntries = [...countByCategory(items as Parameters<typeof countByCategory>[0]).entries()]
|
|
100
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
101
|
+
.slice(0, 12)
|
|
102
|
+
.map(([name, count]) => ({ name, slug: name, count }));
|
|
103
|
+
const stackEntries = [...countByStack(items as Parameters<typeof countByStack>[0]).entries()]
|
|
104
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
105
|
+
.slice(0, 18)
|
|
106
|
+
.map(([name, count]) => ({ name, slug: name, count }));
|
|
107
|
+
|
|
108
|
+
function hrefForPage(target: number): string {
|
|
109
|
+
const next: AppsFilters = { ...filters, page: target };
|
|
110
|
+
const query = searchParamsFromFilters(next).toString();
|
|
111
|
+
return query ? `/${slug}?${query}` : `/${slug}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hrefForRemove(key: keyof AppsFilters, value: string): string {
|
|
115
|
+
const params = new URLSearchParams(Astro.url.searchParams);
|
|
116
|
+
params.delete("page");
|
|
117
|
+
const urlKey: Partial<Record<keyof AppsFilters, string>> = {
|
|
118
|
+
stacks: "stack",
|
|
119
|
+
platforms: "platform",
|
|
120
|
+
categories: "category",
|
|
121
|
+
labels: "label",
|
|
122
|
+
licenses: "license",
|
|
123
|
+
statuses: "status",
|
|
124
|
+
lens: "lens",
|
|
125
|
+
q: "q",
|
|
126
|
+
};
|
|
127
|
+
const target = urlKey[key];
|
|
128
|
+
if (target === "status") {
|
|
129
|
+
const next = (params.get("status") ?? "")
|
|
130
|
+
.split(",")
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.filter((entry) => entry !== value);
|
|
133
|
+
params.delete("status");
|
|
134
|
+
if (next.length) params.set("status", next.join(","));
|
|
135
|
+
} else if (target) {
|
|
136
|
+
const values = params.getAll(target).filter((entry) => entry !== value);
|
|
137
|
+
params.delete(target);
|
|
138
|
+
for (const entry of values) params.append(target, entry);
|
|
139
|
+
}
|
|
140
|
+
const query = params.toString();
|
|
141
|
+
return query ? `/${slug}?${query}` : `/${slug}`;
|
|
142
|
+
}
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
<BaseLayout
|
|
146
|
+
title={title}
|
|
147
|
+
description={description}
|
|
148
|
+
site={siteConfig}
|
|
149
|
+
submitHref="/submit"
|
|
150
|
+
submitLabel={`Submit ${itemLabel()}`}
|
|
151
|
+
>
|
|
152
|
+
<section class="border-b border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950">
|
|
153
|
+
<div class="grove-container-wide mx-auto px-5 pt-10 sm:px-7 sm:pt-12">
|
|
154
|
+
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
155
|
+
{slug}
|
|
156
|
+
</span>
|
|
157
|
+
<h1 class="m-0 mt-1 text-[1.875rem] sm:text-[2.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
158
|
+
Browse {slug}
|
|
159
|
+
</h1>
|
|
160
|
+
<p class="m-0 mt-2 max-w-[64ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
161
|
+
Search, filter, and explore {total} {itemLabel()}{total === 1 ? "" : "s"} in this directory.
|
|
162
|
+
</p>
|
|
163
|
+
|
|
164
|
+
<form
|
|
165
|
+
method="get"
|
|
166
|
+
action={`/${slug}`}
|
|
167
|
+
class="mt-6 flex flex-col gap-2 sm:flex-row"
|
|
168
|
+
role="search"
|
|
169
|
+
>
|
|
170
|
+
<label class="relative flex-1">
|
|
171
|
+
<span class="sr-only">{`Search ${itemLabelPlural()}`}</span>
|
|
172
|
+
<svg
|
|
173
|
+
viewBox="0 0 16 16"
|
|
174
|
+
width="16"
|
|
175
|
+
height="16"
|
|
176
|
+
aria-hidden="true"
|
|
177
|
+
class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-ink-400"
|
|
178
|
+
fill="currentColor"
|
|
179
|
+
>
|
|
180
|
+
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.75.75 0 0 1-1.06 1.06l-3.04-3.04ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z" />
|
|
181
|
+
</svg>
|
|
182
|
+
<input
|
|
183
|
+
type="search"
|
|
184
|
+
name="q"
|
|
185
|
+
value={filters.q ?? ""}
|
|
186
|
+
placeholder={`Search ${itemLabelPlural()}, stacks, categories, platforms, licenses...`}
|
|
187
|
+
class="h-10 w-full rounded-md border border-ink-200 bg-white py-2 pl-9 pr-3 text-sm text-ink-900 placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100"
|
|
188
|
+
/>
|
|
189
|
+
</label>
|
|
190
|
+
{filters.sort && <input type="hidden" name="sort" value={filters.sort} />}
|
|
191
|
+
<button type="submit" class="grove-btn grove-btn-primary">Search</button>
|
|
192
|
+
</form>
|
|
193
|
+
|
|
194
|
+
<div class="mt-4">
|
|
195
|
+
<SmartLensTabs
|
|
196
|
+
currentParams={Astro.url.searchParams}
|
|
197
|
+
pathPrefix={`/${slug}`}
|
|
198
|
+
lensIds={["all", "new", "hot", "mature", "production-like", "good-to-learn"]}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="mt-3 pb-6">
|
|
203
|
+
<RefinePanel
|
|
204
|
+
facets={facets}
|
|
205
|
+
initial={filters}
|
|
206
|
+
pathPrefix={`/${slug}`}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
{categoryEntries.length > 0 && (
|
|
213
|
+
<ExploreByCategory
|
|
214
|
+
categories={categoryEntries}
|
|
215
|
+
pathPrefix={`/${slug}`}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{stackEntries.length > 0 && (
|
|
220
|
+
<ExploreByStack
|
|
221
|
+
stacks={stackEntries}
|
|
222
|
+
pathPrefix={`/${slug}`}
|
|
223
|
+
/>
|
|
224
|
+
)}
|
|
225
|
+
|
|
226
|
+
<section class="grove-section-tight">
|
|
227
|
+
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
228
|
+
<header class="mb-6">
|
|
229
|
+
<h2 class="m-0 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
230
|
+
{`All ${itemLabelPlural()}`}
|
|
231
|
+
</h2>
|
|
232
|
+
<p class="m-0 mt-1 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
|
|
233
|
+
{`Every ${itemLabel()} in the directory.`}
|
|
234
|
+
</p>
|
|
235
|
+
</header>
|
|
236
|
+
<div class="mb-5 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
237
|
+
<p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
|
|
238
|
+
<span id="results-count" class="font-mono text-ink-900 dark:text-ink-100">
|
|
239
|
+
{sorted.length} {sorted.length === 1 ? itemLabel() : itemLabelPlural()}
|
|
240
|
+
</span>
|
|
241
|
+
<span id="results-sort">{` · sorted by ${sortDisplay(sort)}`}</span>
|
|
242
|
+
<span id="results-page">{pages > 1 ? ` · page ${page} of ${pages}` : ""}</span>
|
|
243
|
+
</p>
|
|
244
|
+
<div
|
|
245
|
+
id="active-filters"
|
|
246
|
+
class:list={["flex flex-wrap items-center gap-1.5", chips.length === 0 && "hidden"]}
|
|
247
|
+
aria-label="Active filters"
|
|
248
|
+
>
|
|
249
|
+
{chips.length > 0 && (
|
|
250
|
+
<>
|
|
251
|
+
{chips.map((chip) => (
|
|
252
|
+
<a
|
|
253
|
+
href={hrefForRemove(chip.key, chip.value)}
|
|
254
|
+
class="grove-badge transition-colors hover:border-ink-400"
|
|
255
|
+
>
|
|
256
|
+
{chip.label} <span aria-hidden="true">x</span>
|
|
257
|
+
</a>
|
|
258
|
+
))}
|
|
259
|
+
<a href={`/${slug}`} class="text-2xs font-medium text-ink-500 hover:text-ink-900">
|
|
260
|
+
Clear all
|
|
261
|
+
</a>
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<ul id="results-list" class="m-0 flex list-none flex-col gap-3 p-0">
|
|
268
|
+
{sorted.map((item) => (
|
|
269
|
+
<li class="m-0 p-0" data-item-slug={item.slug}>
|
|
270
|
+
<AppsIndexRow record={item as Parameters<typeof AppsIndexRow>[0]["record"]} detailHref={`/${slug}/${item.slug}`} />
|
|
271
|
+
</li>
|
|
272
|
+
))}
|
|
273
|
+
</ul>
|
|
274
|
+
<div id="empty-state" class:list={["py-12 text-center", sorted.length > 0 && "hidden"]}>
|
|
275
|
+
<p class="m-0 text-sm font-medium text-ink-700 dark:text-ink-200">
|
|
276
|
+
{`No ${itemLabel()}s match these filters.`}
|
|
277
|
+
</p>
|
|
278
|
+
<a href={`/${slug}`} class="grove-btn grove-btn-outline mt-4">Clear filters</a>
|
|
279
|
+
</div>
|
|
280
|
+
<nav id="client-pagination" class="mt-8 flex flex-wrap items-center justify-center gap-1.5" aria-label="Pagination"></nav>
|
|
281
|
+
</div>
|
|
282
|
+
</section>
|
|
283
|
+
|
|
284
|
+
<script is:inline define:vars={{ clientItems, slug, itemSingular: itemLabel(), itemPlural: itemLabelPlural() }}>
|
|
285
|
+
(function () {
|
|
286
|
+
var PAGE_SIZE = 20;
|
|
287
|
+
var list = document.getElementById("results-list");
|
|
288
|
+
if (!list) return;
|
|
289
|
+
|
|
290
|
+
function values(params, key) {
|
|
291
|
+
return params.getAll(key).filter(Boolean);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function display(value) {
|
|
295
|
+
return String(value || "")
|
|
296
|
+
.split(/[-_]+/)
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.map(function (part) { return part.charAt(0).toUpperCase() + part.slice(1); })
|
|
299
|
+
.join(" ");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function sortLabel(value) {
|
|
303
|
+
return {
|
|
304
|
+
"recently-updated": "Recently updated",
|
|
305
|
+
"most-starred": "Most starred",
|
|
306
|
+
"recently-added": "Recently added",
|
|
307
|
+
"best-overall": "Best overall",
|
|
308
|
+
alphabetical: "Alphabetical",
|
|
309
|
+
}[value] || "Recently updated";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function pageHref(params, page) {
|
|
313
|
+
var next = new URLSearchParams(params);
|
|
314
|
+
if (page <= 1) next.delete("page");
|
|
315
|
+
else next.set("page", String(page));
|
|
316
|
+
var query = next.toString();
|
|
317
|
+
return query ? "/" + slug + "?" + query : "/" + slug;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function apply() {
|
|
321
|
+
var params = new URLSearchParams(location.search);
|
|
322
|
+
var query = (params.get("q") || "").trim().toLowerCase();
|
|
323
|
+
var stacks = values(params, "stack");
|
|
324
|
+
var platforms = values(params, "platform");
|
|
325
|
+
var categories = values(params, "category");
|
|
326
|
+
var labels = values(params, "label");
|
|
327
|
+
var licenses = values(params, "license");
|
|
328
|
+
var statuses = (params.get("status") || "").split(",").filter(Boolean);
|
|
329
|
+
var lens = params.get("lens") || "";
|
|
330
|
+
var sort = params.get("sort") || "recently-updated";
|
|
331
|
+
var page = Math.max(1, Number(params.get("page")) || 1);
|
|
332
|
+
|
|
333
|
+
var filtered = clientItems.filter(function (item) {
|
|
334
|
+
var haystack = [
|
|
335
|
+
item.name,
|
|
336
|
+
item.description,
|
|
337
|
+
item.category,
|
|
338
|
+
item.tags,
|
|
339
|
+
item.stacks,
|
|
340
|
+
item.platforms,
|
|
341
|
+
item.license,
|
|
342
|
+
item.status,
|
|
343
|
+
item.extra,
|
|
344
|
+
].flat(3).join(" ").toLowerCase();
|
|
345
|
+
if (query && !haystack.includes(query)) return false;
|
|
346
|
+
if (stacks.length && !item.stacks.some(function (value) { return stacks.includes(value); })) return false;
|
|
347
|
+
if (platforms.length && !item.platforms.some(function (value) { return platforms.includes(value); })) return false;
|
|
348
|
+
if (categories.length && !categories.includes(item.category)) return false;
|
|
349
|
+
if (labels.length && !item.labels.some(function (value) { return labels.includes(value); })) return false;
|
|
350
|
+
if (licenses.length && !licenses.includes(item.license)) return false;
|
|
351
|
+
if (statuses.length && !statuses.includes(item.status)) return false;
|
|
352
|
+
if (lens && !item.lenses.includes(lens)) return false;
|
|
353
|
+
return true;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
filtered.sort(function (a, b) {
|
|
357
|
+
if (sort === "most-starred") return b.stars - a.stars;
|
|
358
|
+
if (sort === "recently-added") return Date.parse(b.addedAt || 0) - Date.parse(a.addedAt || 0);
|
|
359
|
+
if (sort === "best-overall") return Number(b.reviewed) - Number(a.reviewed) || b.stars - a.stars;
|
|
360
|
+
if (sort === "alphabetical") return a.name.localeCompare(b.name);
|
|
361
|
+
return Date.parse(b.updatedAt || 0) - Date.parse(a.updatedAt || 0);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
|
365
|
+
page = Math.min(page, totalPages);
|
|
366
|
+
var visible = new Set(
|
|
367
|
+
filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE).map(function (item) { return item.slug; }),
|
|
368
|
+
);
|
|
369
|
+
var order = new Map(filtered.map(function (item, index) { return [item.slug, index]; }));
|
|
370
|
+
Array.from(list.children)
|
|
371
|
+
.sort(function (a, b) {
|
|
372
|
+
return (order.get(a.dataset.itemSlug) ?? Number.MAX_SAFE_INTEGER) -
|
|
373
|
+
(order.get(b.dataset.itemSlug) ?? Number.MAX_SAFE_INTEGER);
|
|
374
|
+
})
|
|
375
|
+
.forEach(function (node) {
|
|
376
|
+
node.hidden = !visible.has(node.dataset.itemSlug);
|
|
377
|
+
list.appendChild(node);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
var count = document.getElementById("results-count");
|
|
381
|
+
if (count) count.textContent = filtered.length + " " + (filtered.length === 1 ? itemSingular : itemPlural);
|
|
382
|
+
var sortText = document.getElementById("results-sort");
|
|
383
|
+
if (sortText) sortText.textContent = " · sorted by " + sortLabel(sort);
|
|
384
|
+
var pageText = document.getElementById("results-page");
|
|
385
|
+
if (pageText) pageText.textContent = totalPages > 1 ? " · page " + page + " of " + totalPages : "";
|
|
386
|
+
document.getElementById("empty-state")?.classList.toggle("hidden", filtered.length > 0);
|
|
387
|
+
|
|
388
|
+
var search = document.querySelector('input[name="q"]');
|
|
389
|
+
if (search) search.value = params.get("q") || "";
|
|
390
|
+
document.querySelectorAll("select").forEach(function (select) {
|
|
391
|
+
var selected = values(params, select.name);
|
|
392
|
+
if (select.name === "sort") selected = [sort];
|
|
393
|
+
Array.from(select.options).forEach(function (option) {
|
|
394
|
+
option.selected = selected.includes(option.value);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
var pager = document.getElementById("client-pagination");
|
|
399
|
+
if (pager) {
|
|
400
|
+
pager.innerHTML = "";
|
|
401
|
+
if (totalPages > 1) {
|
|
402
|
+
for (var number = 1; number <= totalPages; number += 1) {
|
|
403
|
+
var link = document.createElement("a");
|
|
404
|
+
link.href = pageHref(params, number);
|
|
405
|
+
link.textContent = String(number);
|
|
406
|
+
link.className = "grove-btn min-w-9 px-2 py-1.5 text-2xs " +
|
|
407
|
+
(number === page ? "grove-btn-primary" : "grove-btn-outline");
|
|
408
|
+
if (number === page) link.setAttribute("aria-current", "page");
|
|
409
|
+
pager.appendChild(link);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
var chipHost = document.getElementById("active-filters");
|
|
415
|
+
if (chipHost) {
|
|
416
|
+
chipHost.innerHTML = "";
|
|
417
|
+
var specs = [
|
|
418
|
+
["q", params.get("q") ? "“" + params.get("q") + "”" : ""],
|
|
419
|
+
...stacks.map(function (v) { return ["stack", "Stack: " + v]; }),
|
|
420
|
+
...platforms.map(function (v) { return ["platform", "Platform: " + v]; }),
|
|
421
|
+
...categories.map(function (v) { return ["category", "Category: " + v]; }),
|
|
422
|
+
...labels.map(function (v) { return ["label", display(v)]; }),
|
|
423
|
+
...licenses.map(function (v) { return ["license", "License: " + v]; }),
|
|
424
|
+
...(lens ? [["lens", display(lens)]] : []),
|
|
425
|
+
...statuses.map(function (v) { return ["status", display(v)]; }),
|
|
426
|
+
].filter(function (entry) { return entry[1]; });
|
|
427
|
+
specs.forEach(function (entry) {
|
|
428
|
+
var key = entry[0];
|
|
429
|
+
var chip = document.createElement("a");
|
|
430
|
+
var next = new URLSearchParams(params);
|
|
431
|
+
if (key === "status") {
|
|
432
|
+
var remaining = statuses.filter(function (value) { return display(value) !== entry[1]; });
|
|
433
|
+
next.delete("status");
|
|
434
|
+
if (remaining.length) next.set("status", remaining.join(","));
|
|
435
|
+
} else {
|
|
436
|
+
next.delete(key);
|
|
437
|
+
}
|
|
438
|
+
next.delete("page");
|
|
439
|
+
chip.href = next.toString() ? "/" + slug + "?" + next : "/" + slug;
|
|
440
|
+
chip.className = "grove-badge transition-colors hover:border-ink-400";
|
|
441
|
+
chip.textContent = entry[1] + " x";
|
|
442
|
+
chipHost.appendChild(chip);
|
|
443
|
+
});
|
|
444
|
+
if (specs.length) {
|
|
445
|
+
var clear = document.createElement("a");
|
|
446
|
+
clear.href = "/" + slug;
|
|
447
|
+
clear.className = "text-2xs font-medium text-ink-500 hover:text-ink-900";
|
|
448
|
+
clear.textContent = "Clear all";
|
|
449
|
+
chipHost.appendChild(clear);
|
|
450
|
+
}
|
|
451
|
+
chipHost.classList.toggle("hidden", specs.length === 0);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
document.querySelectorAll("form").forEach(function (form) {
|
|
456
|
+
form.addEventListener("submit", function (event) {
|
|
457
|
+
if (!form.closest("main")) return;
|
|
458
|
+
event.preventDefault();
|
|
459
|
+
var params = new URLSearchParams(location.search);
|
|
460
|
+
form.querySelectorAll("input, select").forEach(function (field) {
|
|
461
|
+
if (!field.name) return;
|
|
462
|
+
params.delete(field.name);
|
|
463
|
+
if (field instanceof HTMLSelectElement && field.multiple) {
|
|
464
|
+
Array.from(field.selectedOptions).forEach(function (option) { params.append(field.name, option.value); });
|
|
465
|
+
} else if (field.value) {
|
|
466
|
+
params.set(field.name, field.value);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
params.delete("page");
|
|
470
|
+
location.assign(params.toString() ? "?" + params : location.pathname);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
apply();
|
|
474
|
+
})();
|
|
475
|
+
</script>
|
|
476
|
+
</BaseLayout>
|