@grove-dev/astro 0.2.13 → 0.2.16
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/README.md +26 -18
- package/dist/lib/repo.d.ts +2 -1
- package/dist/lib/repo.d.ts.map +1 -1
- package/dist/lib/repo.js +2 -1
- package/dist/lib/repo.js.map +1 -1
- package/dist/lib/search.d.ts +13 -16
- package/dist/lib/search.d.ts.map +1 -1
- package/dist/lib/search.js +6 -9
- package/dist/lib/search.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ItemCard.astro +33 -5
- package/src/components/RecordSection.astro +2 -1
- package/src/components/RefinePanel.astro +3 -3
- package/src/lib/repo.ts +2 -1
- package/src/lib/search.ts +26 -31
- package/src/styles.css +3 -1
- package/templates/default/package.json +3 -3
- package/templates/default/public/llms-full.txt +1 -1
- package/templates/default/public/sitemap.xml +10 -10
- package/templates/default/src/data/records.ts +8 -4
- package/templates/default/src/pages/[slug]/{[itemSlug].astro → [recordSlug].astro} +50 -3
- package/templates/default/src/pages/[slug]/index.astro +111 -33
- package/templates/default/src/pages/apps/[recordSlug].astro +33 -0
- package/templates/default/src/pages/apps/[itemSlug].astro +0 -301
- package/templates/default/src/pages/apps/index.astro +0 -146
- /package/src/components/{AppsIndexRow.astro → IndexRow.astro} +0 -0
- /package/src/components/{AppsPagination.astro → Pagination.astro} +0 -0
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /apps/<slug> — alternate detail URL.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors `/[slug]/[itemSlug].astro` (the blueprint-aware dynamic
|
|
6
|
-
* detail page) at a static URL. For non-project blueprints
|
|
7
|
-
* (resource-hub, ecosystem-map), redirect to the matching dynamic
|
|
8
|
-
* route.
|
|
9
|
-
*/
|
|
10
|
-
import { indexSlug, fullRecords, projectBySlug, recordBySlug } from "../../data/records";
|
|
11
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
12
|
-
import Icon from "@grove-dev/astro/components/Icon.astro";
|
|
13
|
-
import ScoreBars from "@grove-dev/astro/components/ScoreBars.astro";
|
|
14
|
-
import siteConfig from "../../../data/generated/site-config.json";
|
|
15
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl, formatRelative, formatStars, statusDisplay, prettySlug, labelDisplay } from "@grove-dev/astro";
|
|
16
|
-
|
|
17
|
-
export async function getStaticPaths() {
|
|
18
|
-
return fullRecords.map((r) => ({ params: { itemSlug: r.slug } }));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const slug = indexSlug();
|
|
22
|
-
const itemSlug = Astro.params.itemSlug ?? "";
|
|
23
|
-
|
|
24
|
-
if (slug !== "projects") {
|
|
25
|
-
return Astro.redirect(`/${slug}/${itemSlug}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const record = recordBySlug(itemSlug);
|
|
29
|
-
if (!record) {
|
|
30
|
-
return Astro.redirect("/apps");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const project = projectBySlug(itemSlug);
|
|
34
|
-
const isProject = record.kind === "project";
|
|
35
|
-
const name = record.kind === "resource" ? record.title : record.name;
|
|
36
|
-
const repoUrl = project?.repoUrl ?? (record.links as { github?: string } | undefined)?.github ?? "";
|
|
37
|
-
const homepageUrl = record.links?.website ?? "";
|
|
38
|
-
const logoUrl = project?.logoUrl;
|
|
39
|
-
const stacks = project?.stacks ?? (project?.stack ? [project.stack] : []);
|
|
40
|
-
const platforms = project?.platforms ?? [];
|
|
41
|
-
const github = project?.github?.repository;
|
|
42
|
-
const stars = github?.stargazers_count ?? 0;
|
|
43
|
-
const starsLabel = formatStars(stars);
|
|
44
|
-
const pushedAt = github?.pushed_at ?? null;
|
|
45
|
-
const pushedLabel = formatRelative(pushedAt);
|
|
46
|
-
const ownerRepo = repoUrl ? getOwnerAndRepoFromRepoUrl(repoUrl) : null;
|
|
47
|
-
const avatarSrc = logoUrl ?? (ownerRepo ? getOwnerAvatarUrl(ownerRepo.owner, 80) : null);
|
|
48
|
-
const initials = (name || "").replace(/[^a-z0-9]/gi, "").slice(0, 2).toUpperCase();
|
|
49
|
-
const bestFor = project?.bestFor ?? [];
|
|
50
|
-
const whyListed = project?.whyListed ?? [];
|
|
51
|
-
const caveats = project?.caveats ?? [];
|
|
52
|
-
const distribution = project?.distribution?.channels ?? [];
|
|
53
|
-
const scores = project?.scores ?? null;
|
|
54
|
-
const curationLabels = record.curation?.labels ?? [];
|
|
55
|
-
const tags = record.tags ?? [];
|
|
56
|
-
const healthStatus = project?.health?.status;
|
|
57
|
-
const healthLabel = healthStatus ? statusDisplay(healthStatus) : null;
|
|
58
|
-
|
|
59
|
-
const title = `${name} — ${siteConfig.name}`;
|
|
60
|
-
const description = record.description ?? `${name} on ${siteConfig.name}`;
|
|
61
|
-
|
|
62
|
-
const jsonLd: Record<string, unknown> | null = project
|
|
63
|
-
? {
|
|
64
|
-
"@context": "https://schema.org",
|
|
65
|
-
"@type": "SoftwareSourceCode",
|
|
66
|
-
name,
|
|
67
|
-
description: record.description ?? undefined,
|
|
68
|
-
url: repoUrl || homepageUrl || undefined,
|
|
69
|
-
codeRepository: repoUrl || undefined,
|
|
70
|
-
programmingLanguage: github?.language ?? undefined,
|
|
71
|
-
license: github?.license?.spdx_id ?? undefined,
|
|
72
|
-
author: ownerRepo
|
|
73
|
-
? { "@type": "Organization", name: ownerRepo.owner, url: `https://github.com/${ownerRepo.owner}` }
|
|
74
|
-
: undefined,
|
|
75
|
-
}
|
|
76
|
-
: null;
|
|
77
|
-
---
|
|
78
|
-
|
|
79
|
-
<BaseLayout
|
|
80
|
-
title={title}
|
|
81
|
-
description={description}
|
|
82
|
-
site={siteConfig}
|
|
83
|
-
type="article"
|
|
84
|
-
jsonLd={jsonLd ?? undefined}
|
|
85
|
-
submitHref="/submit"
|
|
86
|
-
submitLabel={`Submit ${name}`}
|
|
87
|
-
>
|
|
88
|
-
<article class="grove-section">
|
|
89
|
-
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
90
|
-
<nav class="mb-6 text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
|
|
91
|
-
<a href="/apps" class="hover:text-ink-900 dark:hover:text-ink-100">Apps</a>
|
|
92
|
-
<span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
|
|
93
|
-
<span class="text-ink-700 dark:text-ink-300">{name}</span>
|
|
94
|
-
</nav>
|
|
95
|
-
|
|
96
|
-
<header class="grove-card flex flex-col gap-5 p-6 sm:flex-row sm:items-start">
|
|
97
|
-
<div class="flex-shrink-0">
|
|
98
|
-
{avatarSrc ? (
|
|
99
|
-
<img
|
|
100
|
-
src={avatarSrc}
|
|
101
|
-
alt=""
|
|
102
|
-
width="64"
|
|
103
|
-
height="64"
|
|
104
|
-
loading="lazy"
|
|
105
|
-
decoding="async"
|
|
106
|
-
class="h-16 w-16 rounded-lg border border-ink-200 object-contain bg-white p-1 dark:border-ink-800 dark:bg-ink-950"
|
|
107
|
-
/>
|
|
108
|
-
) : (
|
|
109
|
-
<span
|
|
110
|
-
aria-hidden="true"
|
|
111
|
-
class="inline-flex h-16 w-16 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-2xl font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900 dark:text-ink-200"
|
|
112
|
-
>
|
|
113
|
-
{initials || "·"}
|
|
114
|
-
</span>
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div class="min-w-0 flex-1">
|
|
119
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
120
|
-
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
121
|
-
{record.kind}
|
|
122
|
-
</span>
|
|
123
|
-
{record.category && (
|
|
124
|
-
<span class="rounded-full border border-ink-200 px-2 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:text-ink-300">
|
|
125
|
-
{record.category}
|
|
126
|
-
</span>
|
|
127
|
-
)}
|
|
128
|
-
{curationLabels.map((l) => (
|
|
129
|
-
<span class="grove-badge" title={`Curated: ${labelDisplay(l) ?? l}`}>
|
|
130
|
-
{labelDisplay(l) ?? prettySlug(l)}
|
|
131
|
-
</span>
|
|
132
|
-
))}
|
|
133
|
-
{healthLabel && (
|
|
134
|
-
<span class="grove-badge grove-badge-stale">{healthLabel}</span>
|
|
135
|
-
)}
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
<h1 class="m-0 mt-2 text-[1.75rem] sm:text-[2rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
139
|
-
{name}
|
|
140
|
-
</h1>
|
|
141
|
-
|
|
142
|
-
{ownerRepo && (
|
|
143
|
-
<p class="m-0 mt-1 text-sm text-ink-500 dark:text-ink-400">
|
|
144
|
-
<a
|
|
145
|
-
href={`https://github.com/${ownerRepo.owner}`}
|
|
146
|
-
target="_blank"
|
|
147
|
-
rel="noopener noreferrer"
|
|
148
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
149
|
-
>
|
|
150
|
-
@{ownerRepo.owner}
|
|
151
|
-
</a>
|
|
152
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">/</span>
|
|
153
|
-
<a
|
|
154
|
-
href={repoUrl}
|
|
155
|
-
target="_blank"
|
|
156
|
-
rel="noopener noreferrer"
|
|
157
|
-
class="text-inherit no-underline hover:text-ink-900 dark:hover:text-ink-100"
|
|
158
|
-
>
|
|
159
|
-
{ownerRepo.repo}
|
|
160
|
-
</a>
|
|
161
|
-
</p>
|
|
162
|
-
)}
|
|
163
|
-
|
|
164
|
-
{record.description && (
|
|
165
|
-
<p class="m-0 mt-3 max-w-[68ch] text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
166
|
-
{record.description}
|
|
167
|
-
</p>
|
|
168
|
-
)}
|
|
169
|
-
|
|
170
|
-
<div class="mt-5 flex flex-wrap gap-2">
|
|
171
|
-
{repoUrl && (
|
|
172
|
-
<a href={repoUrl} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-primary">
|
|
173
|
-
View on GitHub →
|
|
174
|
-
</a>
|
|
175
|
-
)}
|
|
176
|
-
{homepageUrl && (
|
|
177
|
-
<a href={homepageUrl} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-outline">
|
|
178
|
-
Homepage
|
|
179
|
-
</a>
|
|
180
|
-
)}
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
</header>
|
|
184
|
-
|
|
185
|
-
{project && (
|
|
186
|
-
<section class="mt-8 grove-card p-6" aria-labelledby="scores-heading">
|
|
187
|
-
<h2 id="scores-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
188
|
-
Scores
|
|
189
|
-
</h2>
|
|
190
|
-
<div class="mt-4">
|
|
191
|
-
<ScoreBars scores={scores} />
|
|
192
|
-
</div>
|
|
193
|
-
</section>
|
|
194
|
-
)}
|
|
195
|
-
|
|
196
|
-
{/* BestFor / WhyListed / Caveats grid */}
|
|
197
|
-
{isProject && (bestFor.length > 0 || whyListed.length > 0 || caveats.length > 0) && (
|
|
198
|
-
<section class="mt-8 grid grid-cols-1 gap-4 md:grid-cols-3" aria-label="Curation notes">
|
|
199
|
-
{bestFor.length > 0 && (
|
|
200
|
-
<div class="grove-card p-5">
|
|
201
|
-
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Best for</h3>
|
|
202
|
-
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
203
|
-
{bestFor.map((b) => <li class="text-sm text-ink-700 dark:text-ink-200">— {b}</li>)}
|
|
204
|
-
</ul>
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
{whyListed.length > 0 && (
|
|
208
|
-
<div class="grove-card p-5">
|
|
209
|
-
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Why listed</h3>
|
|
210
|
-
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
211
|
-
{whyListed.map((w) => <li class="text-sm text-ink-700 dark:text-ink-200">— {w}</li>)}
|
|
212
|
-
</ul>
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
{caveats.length > 0 && (
|
|
216
|
-
<div class="grove-card p-5">
|
|
217
|
-
<h3 class="m-0 text-sm font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Caveats</h3>
|
|
218
|
-
<ul class="mt-3 m-0 flex list-none flex-col gap-1.5 p-0">
|
|
219
|
-
{caveats.map((c) => <li class="text-sm text-ink-700 dark:text-ink-200">— {c}</li>)}
|
|
220
|
-
</ul>
|
|
221
|
-
</div>
|
|
222
|
-
)}
|
|
223
|
-
</section>
|
|
224
|
-
)}
|
|
225
|
-
|
|
226
|
-
{/* Stack + Platform chips */}
|
|
227
|
-
{isProject && (stacks.length > 0 || platforms.length > 0) && (
|
|
228
|
-
<section class="mt-8" aria-labelledby="stacks-heading">
|
|
229
|
-
<h2 id="stacks-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
230
|
-
Stack & platforms
|
|
231
|
-
</h2>
|
|
232
|
-
<div class="mt-4 flex flex-col gap-3">
|
|
233
|
-
{stacks.length > 0 && (
|
|
234
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
235
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Stack</span>
|
|
236
|
-
{stacks.map((s) => (
|
|
237
|
-
<span class="grove-badge inline-flex items-center gap-1.5">
|
|
238
|
-
<Icon name={s} category="stack" size={14} />
|
|
239
|
-
{s}
|
|
240
|
-
</span>
|
|
241
|
-
))}
|
|
242
|
-
</div>
|
|
243
|
-
)}
|
|
244
|
-
{platforms.length > 0 && (
|
|
245
|
-
<div class="flex flex-wrap items-center gap-2">
|
|
246
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Platform</span>
|
|
247
|
-
{platforms.map((p) => (
|
|
248
|
-
<span class="grove-badge inline-flex items-center gap-1.5">
|
|
249
|
-
<Icon name={p} category="platform" size={14} />
|
|
250
|
-
{p}
|
|
251
|
-
</span>
|
|
252
|
-
))}
|
|
253
|
-
</div>
|
|
254
|
-
)}
|
|
255
|
-
</div>
|
|
256
|
-
</section>
|
|
257
|
-
)}
|
|
258
|
-
|
|
259
|
-
{/* Tags */}
|
|
260
|
-
{tags.length > 0 && (
|
|
261
|
-
<section class="mt-8" aria-labelledby="tags-heading">
|
|
262
|
-
<h2 id="tags-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
263
|
-
Tags
|
|
264
|
-
</h2>
|
|
265
|
-
<ul class="mt-4 m-0 flex list-none flex-wrap gap-1.5 p-0">
|
|
266
|
-
{tags.map((t) => (
|
|
267
|
-
<li>
|
|
268
|
-
<a href={`/apps?q=${encodeURIComponent(t)}`} class="grove-badge inline-flex items-center gap-1.5">{t}</a>
|
|
269
|
-
</li>
|
|
270
|
-
))}
|
|
271
|
-
</ul>
|
|
272
|
-
</section>
|
|
273
|
-
)}
|
|
274
|
-
|
|
275
|
-
{/* Distribution channels */}
|
|
276
|
-
{distribution.length > 0 && (
|
|
277
|
-
<section class="mt-8" aria-labelledby="dist-heading">
|
|
278
|
-
<h2 id="dist-heading" class="m-0 text-lg font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
279
|
-
Distribution channels
|
|
280
|
-
</h2>
|
|
281
|
-
<ul class="mt-4 m-0 flex list-none flex-col gap-2 p-0">
|
|
282
|
-
{distribution.map((c) => (
|
|
283
|
-
<li class="grove-card flex flex-wrap items-baseline justify-between gap-2 p-3">
|
|
284
|
-
<div class="flex flex-col">
|
|
285
|
-
<span class="text-sm font-semibold text-ink-900 dark:text-ink-100">
|
|
286
|
-
{c.label ?? c.type}
|
|
287
|
-
{c.verified && <span class="ml-2 grove-badge grove-badge-new">Verified</span>}
|
|
288
|
-
</span>
|
|
289
|
-
{c.notes && <span class="text-xs text-ink-500 dark:text-ink-400">{c.notes}</span>}
|
|
290
|
-
</div>
|
|
291
|
-
<a href={c.url} target="_blank" rel="noopener noreferrer" class="grove-btn grove-btn-outline">
|
|
292
|
-
{c.platform ?? c.type} →
|
|
293
|
-
</a>
|
|
294
|
-
</li>
|
|
295
|
-
))}
|
|
296
|
-
</ul>
|
|
297
|
-
</section>
|
|
298
|
-
)}
|
|
299
|
-
</div>
|
|
300
|
-
</article>
|
|
301
|
-
</BaseLayout>
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* /apps — alternate list URL.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors `/[slug]/index.astro` (the blueprint-aware dynamic list
|
|
6
|
-
* page) at a static URL so the openapps-style `/apps` route works
|
|
7
|
-
* for project-directory blueprints without per-blueprint forks.
|
|
8
|
-
*
|
|
9
|
-
* For non-project blueprints (resource-hub → /resources,
|
|
10
|
-
* ecosystem-map → /entities), this page redirects to the matching
|
|
11
|
-
* dynamic route.
|
|
12
|
-
*/
|
|
13
|
-
import { indexSlug, itemLabelPlural, items } from "../../data/records";
|
|
14
|
-
import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
|
|
15
|
-
import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
|
|
16
|
-
import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
|
|
17
|
-
import AppsIndexRow from "@grove-dev/astro/components/AppsIndexRow.astro";
|
|
18
|
-
import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
|
|
19
|
-
import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
|
|
20
|
-
import siteConfig from "../../../data/generated/site-config.json";
|
|
21
|
-
import {
|
|
22
|
-
activeFilterChips,
|
|
23
|
-
applySort,
|
|
24
|
-
buildFacets,
|
|
25
|
-
effectivePage,
|
|
26
|
-
effectiveSort,
|
|
27
|
-
filterApps,
|
|
28
|
-
filtersFromSearchParams,
|
|
29
|
-
paginate,
|
|
30
|
-
totalPages,
|
|
31
|
-
} from "@grove-dev/astro";
|
|
32
|
-
|
|
33
|
-
const slug = indexSlug();
|
|
34
|
-
|
|
35
|
-
// Only render the static /apps route when the active blueprint is
|
|
36
|
-
// project-directory (the openapps-style URL). For other blueprints,
|
|
37
|
-
// redirect to the dynamic `/<slug>/` route.
|
|
38
|
-
if (slug !== "projects") {
|
|
39
|
-
return Astro.redirect(`/${slug}${Astro.url.search}`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const total = items.length;
|
|
43
|
-
const filters = filtersFromSearchParams(Astro.url.searchParams);
|
|
44
|
-
const facets = buildFacets(items);
|
|
45
|
-
const sort = effectiveSort(filters);
|
|
46
|
-
const requestedPage = effectivePage(filters);
|
|
47
|
-
const filtered = filterApps(items, filters);
|
|
48
|
-
const sorted = applySort(filtered, sort);
|
|
49
|
-
const pages = totalPages(sorted.length);
|
|
50
|
-
const page = Math.min(requestedPage, pages);
|
|
51
|
-
const visibleItems = paginate(sorted, page);
|
|
52
|
-
const chips = activeFilterChips(filters);
|
|
53
|
-
const itemPlural = itemLabelPlural();
|
|
54
|
-
|
|
55
|
-
const title = `${itemPlural.charAt(0).toUpperCase()}${itemPlural.slice(1)} — ${siteConfig.name}`;
|
|
56
|
-
const description = `Browse every curated ${itemPlural.replace(/s$/, "")} in the ${siteConfig.name} directory.`;
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
<BaseLayout title={title} description={description} site={siteConfig}>
|
|
60
|
-
<section class="grove-section">
|
|
61
|
-
<div class="grove-container-wide mx-auto px-5 sm:px-7">
|
|
62
|
-
<header class="mb-6">
|
|
63
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
64
|
-
{itemPlural}
|
|
65
|
-
</span>
|
|
66
|
-
<h1 class="m-0 mt-1 text-[1.75rem] sm:text-[2.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
67
|
-
Browse the directory
|
|
68
|
-
</h1>
|
|
69
|
-
<p class="m-0 mt-2 max-w-2xl text-base leading-relaxed text-ink-700 dark:text-ink-200">
|
|
70
|
-
Search and filter every curated {itemPlural.replace(/s$/, "")}. Use the
|
|
71
|
-
lens tabs to focus on a curated angle, or refine by stack,
|
|
72
|
-
platform, license, and status.
|
|
73
|
-
</p>
|
|
74
|
-
</header>
|
|
75
|
-
|
|
76
|
-
<div class="mb-6">
|
|
77
|
-
<SmartLensTabs
|
|
78
|
-
currentParams={Astro.url.searchParams}
|
|
79
|
-
pathPrefix="/apps"
|
|
80
|
-
/>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<div class="mb-8">
|
|
84
|
-
<RefinePanel
|
|
85
|
-
facets={facets}
|
|
86
|
-
initial={filters}
|
|
87
|
-
pathPrefix="/apps"
|
|
88
|
-
/>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{chips.length > 0 && (
|
|
92
|
-
<ul class="mb-6 flex flex-wrap items-center gap-1.5" aria-label="Active filters">
|
|
93
|
-
{chips.map((chip) => (
|
|
94
|
-
<li>
|
|
95
|
-
<a
|
|
96
|
-
href={chip.removeHref}
|
|
97
|
-
class="grove-badge inline-flex items-center gap-1.5 hover:border-ink-300 dark:hover:border-ink-700"
|
|
98
|
-
title={`Remove filter: ${chip.label}`}
|
|
99
|
-
>
|
|
100
|
-
<span>{chip.label}</span>
|
|
101
|
-
<span aria-hidden="true">×</span>
|
|
102
|
-
</a>
|
|
103
|
-
</li>
|
|
104
|
-
))}
|
|
105
|
-
</ul>
|
|
106
|
-
)}
|
|
107
|
-
|
|
108
|
-
{visibleItems.length === 0 ? (
|
|
109
|
-
<p class="grove-card p-8 text-center text-sm text-ink-500 dark:text-ink-400">
|
|
110
|
-
No {itemPlural} match these filters. Try clearing a filter or two.
|
|
111
|
-
</p>
|
|
112
|
-
) : (
|
|
113
|
-
<ul class="m-0 grid list-none grid-cols-1 gap-3 p-0">
|
|
114
|
-
{visibleItems.map((item) => (
|
|
115
|
-
<li class="m-0 p-0">
|
|
116
|
-
<AppsIndexRow record={item as Parameters<typeof AppsIndexRow>[0]["record"]} detailHref={`/apps/${item.slug}`} />
|
|
117
|
-
</li>
|
|
118
|
-
))}
|
|
119
|
-
</ul>
|
|
120
|
-
)}
|
|
121
|
-
</div>
|
|
122
|
-
</section>
|
|
123
|
-
|
|
124
|
-
<ExploreByCategory
|
|
125
|
-
categories={[
|
|
126
|
-
...new Set(
|
|
127
|
-
items
|
|
128
|
-
.filter((i) => i.kind === "project")
|
|
129
|
-
.map((i) => (i as { category?: string }).category ?? "")
|
|
130
|
-
.filter(Boolean),
|
|
131
|
-
),
|
|
132
|
-
].map((name) => ({ name, slug: name }))}
|
|
133
|
-
pathPrefix="/apps"
|
|
134
|
-
/>
|
|
135
|
-
<ExploreByStack
|
|
136
|
-
stacks={[
|
|
137
|
-
...new Set(
|
|
138
|
-
items
|
|
139
|
-
.filter((i) => i.kind === "project")
|
|
140
|
-
.flatMap((i) => [(i as { stack?: string }).stack ?? "", ...(i as { stacks?: string[] }).stacks])
|
|
141
|
-
.filter(Boolean),
|
|
142
|
-
),
|
|
143
|
-
].map((name) => ({ name, slug: name }))}
|
|
144
|
-
pathPrefix="/apps"
|
|
145
|
-
/>
|
|
146
|
-
</BaseLayout>
|
|
File without changes
|
|
File without changes
|