@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
|
-
---
|
|
2
|
-
import type { OpenSourceApp } from "../data/types";
|
|
3
|
-
import Icon from "./icons/Icon.astro";
|
|
4
|
-
import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../lib/repo";
|
|
5
|
-
import { formatStars, formatRelative } from "../lib/format";
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
app: OpenSourceApp;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const { app } = Astro.props;
|
|
12
|
-
|
|
13
|
-
// Stale apps: a repo with no commits in 365+ days is not really
|
|
14
|
-
// "mature" — it's a snapshot. Override the label so we don't show
|
|
15
|
-
// "Mature" on a 6-year-old fork. (See healthFromSignals in
|
|
16
|
-
// scripts/app-schema.mjs for how status is computed.)
|
|
17
|
-
const isStale = app.status === "stale";
|
|
18
|
-
|
|
19
|
-
const label = isStale
|
|
20
|
-
? "stale"
|
|
21
|
-
: app.labels?.find((l) => l === "new" || l === "hot" || l === "mature") ??
|
|
22
|
-
app.labels?.[0];
|
|
23
|
-
|
|
24
|
-
const labelClass = label === "new"
|
|
25
|
-
? "badge-new"
|
|
26
|
-
: label === "hot"
|
|
27
|
-
? "badge-hot"
|
|
28
|
-
: label === "mature"
|
|
29
|
-
? "badge-mature"
|
|
30
|
-
: label === "stale"
|
|
31
|
-
? "badge-stale"
|
|
32
|
-
: "";
|
|
33
|
-
|
|
34
|
-
// Curated = at least one of the hand-written fields is filled in. These
|
|
35
|
-
// are the apps a human actually looked at. Shown as a "verified" mark
|
|
36
|
-
// next to the label badge so it doesn't dominate the card.
|
|
37
|
-
const isCurated = Boolean(
|
|
38
|
-
app.bestFor?.length || app.whyListed?.length || app.caveats?.length || app.scores,
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
// Initials for placeholder logo (first 2 letters of any word in name).
|
|
42
|
-
const initials = app.name
|
|
43
|
-
.split(/\s+/)
|
|
44
|
-
.map((w) => w[0])
|
|
45
|
-
.filter(Boolean)
|
|
46
|
-
.slice(0, 2)
|
|
47
|
-
.join("")
|
|
48
|
-
.toUpperCase();
|
|
49
|
-
|
|
50
|
-
// Resolve a logo URL for this app:
|
|
51
|
-
// 1. Explicit `logoUrl` on the app
|
|
52
|
-
// 2. GitHub owner avatar (extracted from `repoUrl`)
|
|
53
|
-
// 3. Initials placeholder.
|
|
54
|
-
const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
|
|
55
|
-
const logoUrl = app.logoUrl ?? getOwnerAvatarUrl(owner, 80);
|
|
56
|
-
|
|
57
|
-
// Show up to 4 platforms; overflow as +N.
|
|
58
|
-
const visiblePlatforms = app.platforms.slice(0, 4);
|
|
59
|
-
const platformOverflow = app.platforms.length - visiblePlatforms.length;
|
|
60
|
-
|
|
61
|
-
// Show up to 3 stack chips (primary + first 2 secondary).
|
|
62
|
-
const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
|
|
63
|
-
const visibleStacks = allStacks.slice(0, 3);
|
|
64
|
-
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
65
|
-
|
|
66
|
-
const starsLabel = formatStars(app.stars);
|
|
67
|
-
const updatedLabel = formatRelative(app.lastCommitAt);
|
|
68
|
-
|
|
69
|
-
const detailHref = `/apps/${app.slug}`;
|
|
70
|
-
const repoHref = app.repoUrl;
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
<article
|
|
74
|
-
class="card card-hover group relative flex h-full flex-col gap-3 p-4"
|
|
75
|
-
>
|
|
76
|
-
{/* Top row: logo + name + label badge. */}
|
|
77
|
-
<header class="relative z-10 flex items-start gap-3">
|
|
78
|
-
<a
|
|
79
|
-
href={detailHref}
|
|
80
|
-
aria-label={`Open ${app.name} details`}
|
|
81
|
-
class="absolute inset-0 z-0 rounded-lg"
|
|
82
|
-
></a>
|
|
83
|
-
|
|
84
|
-
{logoUrl ? (
|
|
85
|
-
<img
|
|
86
|
-
src={logoUrl}
|
|
87
|
-
alt={`${app.name} avatar`}
|
|
88
|
-
width="40"
|
|
89
|
-
height="40"
|
|
90
|
-
loading="lazy"
|
|
91
|
-
decoding="async"
|
|
92
|
-
class="h-10 w-10 shrink-0 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
93
|
-
/>
|
|
94
|
-
) : (
|
|
95
|
-
<span
|
|
96
|
-
aria-hidden="true"
|
|
97
|
-
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-2xs font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
|
|
98
|
-
>
|
|
99
|
-
{initials}
|
|
100
|
-
</span>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
104
|
-
<h3
|
|
105
|
-
class="m-0 truncate text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100"
|
|
106
|
-
>
|
|
107
|
-
{/* Name → detail page (not GitHub repo). Stacks above the
|
|
108
|
-
stretched link via z-index. */}
|
|
109
|
-
<a
|
|
110
|
-
href={detailHref}
|
|
111
|
-
class="relative z-10 text-inherit no-underline"
|
|
112
|
-
>
|
|
113
|
-
{app.name}
|
|
114
|
-
</a>
|
|
115
|
-
</h3>
|
|
116
|
-
<span class="truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
117
|
-
{owner ? (
|
|
118
|
-
<>
|
|
119
|
-
<a
|
|
120
|
-
href={repoHref}
|
|
121
|
-
target="_blank"
|
|
122
|
-
rel="noopener noreferrer"
|
|
123
|
-
class="relative z-10 font-mono text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
124
|
-
>
|
|
125
|
-
<span class="text-ink-400 dark:text-ink-500">@</span>{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
|
|
126
|
-
</a>
|
|
127
|
-
<span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
|
|
128
|
-
{app.category}
|
|
129
|
-
</>
|
|
130
|
-
) : (
|
|
131
|
-
app.category
|
|
132
|
-
)}
|
|
133
|
-
</span>
|
|
134
|
-
</div>
|
|
135
|
-
|
|
136
|
-
<div class="flex shrink-0 flex-col items-end gap-1">
|
|
137
|
-
{label && (
|
|
138
|
-
<span class={`badge ${labelClass} text-[10px] leading-none`}>
|
|
139
|
-
{label}
|
|
140
|
-
</span>
|
|
141
|
-
)}
|
|
142
|
-
{isCurated && (
|
|
143
|
-
<span
|
|
144
|
-
class="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
|
|
145
|
-
title="Hand-curated entry: bestFor, whyListed, caveats, or scores are filled in"
|
|
146
|
-
>
|
|
147
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
148
|
-
<path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z"/>
|
|
149
|
-
</svg>
|
|
150
|
-
curated
|
|
151
|
-
</span>
|
|
152
|
-
)}
|
|
153
|
-
</div>
|
|
154
|
-
</header>
|
|
155
|
-
|
|
156
|
-
{/* Description */}
|
|
157
|
-
<p
|
|
158
|
-
class="relative z-10 m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400"
|
|
159
|
-
>
|
|
160
|
-
{app.description}
|
|
161
|
-
</p>
|
|
162
|
-
|
|
163
|
-
{/* Metadata row: stars · updated · license — the three signals
|
|
164
|
-
that answer "is this alive and usable?" at a glance. */}
|
|
165
|
-
<div class="relative z-10 flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
166
|
-
{starsLabel !== null && (
|
|
167
|
-
<span class="inline-flex items-center gap-1" title={`${app.stars} stars on GitHub`}>
|
|
168
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
169
|
-
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
|
|
170
|
-
</svg>
|
|
171
|
-
<span class="font-mono">{starsLabel}</span>
|
|
172
|
-
</span>
|
|
173
|
-
)}
|
|
174
|
-
{app.lastCommitAt && (
|
|
175
|
-
<span class="inline-flex items-center gap-1" title={`Last commit ${app.lastCommitAt}`}>
|
|
176
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-ink-400">
|
|
177
|
-
<path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm0 1.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm-.5 1.5a.5.5 0 0 0-1 0v3.25c0 .15.07.29.18.39l1.95 1.65a.5.5 0 0 0 .66-.76L7.5 7.55V5Z"/>
|
|
178
|
-
</svg>
|
|
179
|
-
<span class="font-mono">Updated {updatedLabel}</span>
|
|
180
|
-
</span>
|
|
181
|
-
)}
|
|
182
|
-
{app.license && (
|
|
183
|
-
<span class="inline-flex items-center gap-1" title={`License: ${app.license}`}>
|
|
184
|
-
<span class="font-mono uppercase tracking-wide text-[10px]">{app.license}</span>
|
|
185
|
-
</span>
|
|
186
|
-
)}
|
|
187
|
-
</div>
|
|
188
|
-
|
|
189
|
-
{/* Platform + stack chips, pushed to the bottom. */}
|
|
190
|
-
<div class="relative z-10 mt-auto flex flex-col gap-1.5">
|
|
191
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
192
|
-
{visiblePlatforms.map((p) => (
|
|
193
|
-
<span class="chip">
|
|
194
|
-
<Icon name={p} category="platform" size={11} />
|
|
195
|
-
<span>{p}</span>
|
|
196
|
-
</span>
|
|
197
|
-
))}
|
|
198
|
-
{platformOverflow > 0 && (
|
|
199
|
-
<span class="chip text-ink-400 dark:text-ink-500">+{platformOverflow}</span>
|
|
200
|
-
)}
|
|
201
|
-
</div>
|
|
202
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
203
|
-
{visibleStacks.map((s) => (
|
|
204
|
-
<span class="chip chip-stack">
|
|
205
|
-
<Icon name={s} category="stack" size={11} />
|
|
206
|
-
<span>{s}</span>
|
|
207
|
-
</span>
|
|
208
|
-
))}
|
|
209
|
-
{stackOverflow > 0 && (
|
|
210
|
-
<span class="chip chip-stack text-ink-400 dark:text-ink-500">+{stackOverflow}</span>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
</div>
|
|
214
|
-
|
|
215
|
-
{/* Footer row: View repo + Details links, separated from chips.
|
|
216
|
-
Stacks above the stretched link via z-index. */}
|
|
217
|
-
<footer class="relative z-10 mt-1 flex items-center justify-between gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
218
|
-
<a
|
|
219
|
-
href={repoHref}
|
|
220
|
-
target="_blank"
|
|
221
|
-
rel="noopener noreferrer"
|
|
222
|
-
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-700 transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
223
|
-
>
|
|
224
|
-
View repo
|
|
225
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
226
|
-
<path d="M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Z"/>
|
|
227
|
-
<path d="M10 0a.75.75 0 0 0-.75.75v.75h-3a.75.75 0 0 0 0 1.5h3v.75A.75.75 0 0 0 10.75 4.5h2.5A.75.75 0 0 0 14 3.75v-2.5A.75.75 0 0 0 13.25.5H10Z"/>
|
|
228
|
-
</svg>
|
|
229
|
-
</a>
|
|
230
|
-
<a
|
|
231
|
-
href={detailHref}
|
|
232
|
-
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
233
|
-
>
|
|
234
|
-
Details →
|
|
235
|
-
</a>
|
|
236
|
-
</footer>
|
|
237
|
-
</article>
|
|
238
|
-
|
|
239
|
-
<style>
|
|
240
|
-
@reference "../styles/global.css";
|
|
241
|
-
|
|
242
|
-
.chip {
|
|
243
|
-
@apply inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 transition-colors dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300;
|
|
244
|
-
}
|
|
245
|
-
.chip-stack {
|
|
246
|
-
@apply bg-ink-50 dark:bg-ink-900/60;
|
|
247
|
-
}
|
|
248
|
-
</style>
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { OpenSourceApp } from "../data/types";
|
|
3
|
-
import AppCard from "./AppCard.astro";
|
|
4
|
-
import SectionHeader from "./layout/SectionHeader.astro";
|
|
5
|
-
import Container from "./layout/Container.astro";
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
id: string;
|
|
9
|
-
title: string;
|
|
10
|
-
/** Plain-language subtitle explaining the lens. */
|
|
11
|
-
description: string;
|
|
12
|
-
apps: OpenSourceApp[];
|
|
13
|
-
/** Where the "View all" link points (typically the apps page with a
|
|
14
|
-
* filter applied). */
|
|
15
|
-
viewAllHref?: string;
|
|
16
|
-
/** Custom label for the "View all" link. Lets us say
|
|
17
|
-
* "View all new" / "View hot apps" / "View mature apps"
|
|
18
|
-
* instead of the generic "View all". */
|
|
19
|
-
viewAllLabel?: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
id,
|
|
24
|
-
title,
|
|
25
|
-
description,
|
|
26
|
-
apps,
|
|
27
|
-
viewAllHref = "#",
|
|
28
|
-
viewAllLabel = "View all",
|
|
29
|
-
} = Astro.props;
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
<section id={id} class="section border-b border-ink-200 dark:border-ink-800 scroll-mt-20">
|
|
33
|
-
<Container>
|
|
34
|
-
<div class="flex items-end justify-between gap-4">
|
|
35
|
-
<SectionHeader title={title} description={description} />
|
|
36
|
-
<a
|
|
37
|
-
href={viewAllHref}
|
|
38
|
-
class="hidden shrink-0 pb-2 text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
|
|
39
|
-
>
|
|
40
|
-
{viewAllLabel} →
|
|
41
|
-
</a>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div
|
|
45
|
-
class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
|
|
46
|
-
>
|
|
47
|
-
{apps.map((app) => <AppCard app={app} />)}
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
<div class="mt-4 sm:hidden">
|
|
51
|
-
<a
|
|
52
|
-
href={viewAllHref}
|
|
53
|
-
class="text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
54
|
-
>{viewAllLabel} →</a
|
|
55
|
-
>
|
|
56
|
-
</div>
|
|
57
|
-
</Container>
|
|
58
|
-
</section>
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import type { OpenSourceApp } from "../data/types";
|
|
3
|
-
import Icon from "./icons/Icon.astro";
|
|
4
|
-
import {
|
|
5
|
-
getOwnerAndRepoFromRepoUrl,
|
|
6
|
-
getOwnerAvatarUrl,
|
|
7
|
-
} from "../lib/repo";
|
|
8
|
-
import { formatStars, formatRelative } from "../lib/format";
|
|
9
|
-
|
|
10
|
-
interface Props {
|
|
11
|
-
app: OpenSourceApp;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const { app } = Astro.props;
|
|
15
|
-
|
|
16
|
-
const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
|
|
17
|
-
const avatar = app.logoUrl ?? getOwnerAvatarUrl(owner, 80);
|
|
18
|
-
|
|
19
|
-
// "Mature" wins over "Hot" wins over "New" wins over anything else.
|
|
20
|
-
const label =
|
|
21
|
-
app.labels?.find((l) => l === "mature" || l === "hot" || l === "new") ??
|
|
22
|
-
app.labels?.[0];
|
|
23
|
-
|
|
24
|
-
const labelClass = label === "new"
|
|
25
|
-
? "border-emerald-300/60 bg-emerald-50 text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300"
|
|
26
|
-
: label === "hot"
|
|
27
|
-
? "border-orange-300/60 bg-orange-50 text-orange-800 dark:border-orange-800/60 dark:bg-orange-950/40 dark:text-orange-300"
|
|
28
|
-
: label === "mature"
|
|
29
|
-
? "border-blue-300/60 bg-blue-50 text-blue-800 dark:border-blue-800/60 dark:bg-blue-950/40 dark:text-blue-300"
|
|
30
|
-
: "border-ink-200 bg-ink-50 text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300";
|
|
31
|
-
|
|
32
|
-
const isCurated = Boolean(
|
|
33
|
-
app.bestFor?.length || app.whyListed?.length || app.caveats?.length || app.scores,
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Initials for placeholder logo.
|
|
37
|
-
const initials = app.name
|
|
38
|
-
.split(/\s+/)
|
|
39
|
-
.map((w) => w[0])
|
|
40
|
-
.filter(Boolean)
|
|
41
|
-
.slice(0, 2)
|
|
42
|
-
.join("")
|
|
43
|
-
.toUpperCase();
|
|
44
|
-
|
|
45
|
-
const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
|
|
46
|
-
const visibleStacks = allStacks.slice(0, 4);
|
|
47
|
-
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
48
|
-
|
|
49
|
-
const visiblePlatforms = app.platforms.slice(0, 4);
|
|
50
|
-
const platformOverflow = app.platforms.length - visiblePlatforms.length;
|
|
51
|
-
|
|
52
|
-
const starsLabel = formatStars(app.stars);
|
|
53
|
-
const updatedLabel = formatRelative(app.lastCommitAt);
|
|
54
|
-
|
|
55
|
-
const detailHref = `/apps/${app.slug}`;
|
|
56
|
-
const repoHref = app.repoUrl;
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
<article
|
|
60
|
-
class="apps-row group relative flex flex-col gap-3 rounded-lg border border-ink-200 bg-white p-4 transition-colors hover:border-ink-300 hover:shadow-[0_1px_2px_rgba(0,0,0,0.04),0_4px_12px_-2px_rgba(0,0,0,0.06)] dark:border-ink-800 dark:bg-ink-950 dark:hover:border-ink-700 dark:hover:shadow-[0_1px_2px_rgba(0,0,0,0.4),0_4px_12px_-2px_rgba(0,0,0,0.3)]"
|
|
61
|
-
data-slug={app.slug}
|
|
62
|
-
data-stack={app.stack}
|
|
63
|
-
data-stacks={allStacks.join("|")}
|
|
64
|
-
data-category={app.category ?? ""}
|
|
65
|
-
data-platforms={(app.platforms ?? []).join("|")}
|
|
66
|
-
data-labels={(app.labels ?? []).join("|")}
|
|
67
|
-
data-status={app.status ?? ""}
|
|
68
|
-
data-license={app.license ?? ""}
|
|
69
|
-
data-stars={app.stars ?? 0}
|
|
70
|
-
data-updated={app.lastCommitAt ?? ""}
|
|
71
|
-
data-name={app.name.toLowerCase()}
|
|
72
|
-
data-owner={(owner ?? "").toLowerCase()}
|
|
73
|
-
data-q={(app.description ?? "").toLowerCase()}
|
|
74
|
-
>
|
|
75
|
-
{/* Header: avatar + name + label */}
|
|
76
|
-
<header class="flex items-start gap-3">
|
|
77
|
-
<a
|
|
78
|
-
href={detailHref}
|
|
79
|
-
class="relative z-10 flex shrink-0"
|
|
80
|
-
aria-label={`Open ${app.name} details`}
|
|
81
|
-
>
|
|
82
|
-
{avatar ? (
|
|
83
|
-
<img
|
|
84
|
-
src={avatar}
|
|
85
|
-
alt={`${app.name} avatar`}
|
|
86
|
-
width="44"
|
|
87
|
-
height="44"
|
|
88
|
-
loading="lazy"
|
|
89
|
-
decoding="async"
|
|
90
|
-
class="h-11 w-11 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
91
|
-
/>
|
|
92
|
-
) : (
|
|
93
|
-
<span
|
|
94
|
-
aria-hidden="true"
|
|
95
|
-
class="flex h-11 w-11 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-sm font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
|
|
96
|
-
>
|
|
97
|
-
{initials}
|
|
98
|
-
</span>
|
|
99
|
-
)}
|
|
100
|
-
</a>
|
|
101
|
-
|
|
102
|
-
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
103
|
-
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
104
|
-
<h3 class="m-0 text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
105
|
-
<a
|
|
106
|
-
href={detailHref}
|
|
107
|
-
class="relative z-10 text-inherit no-underline hover:underline"
|
|
108
|
-
>
|
|
109
|
-
{app.name}
|
|
110
|
-
</a>
|
|
111
|
-
</h3>
|
|
112
|
-
{label && (
|
|
113
|
-
<span
|
|
114
|
-
class={`inline-flex items-center rounded border px-1.5 py-0 text-[10px] font-medium uppercase tracking-wider ${labelClass}`}
|
|
115
|
-
>
|
|
116
|
-
{label}
|
|
117
|
-
</span>
|
|
118
|
-
)}
|
|
119
|
-
{isCurated && (
|
|
120
|
-
<span
|
|
121
|
-
class="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
|
|
122
|
-
title="Hand-curated entry"
|
|
123
|
-
>
|
|
124
|
-
<svg viewBox="0 0 16 16" width="9" height="9" aria-hidden="true" fill="currentColor">
|
|
125
|
-
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
|
|
126
|
-
</svg>
|
|
127
|
-
curated
|
|
128
|
-
</span>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
<div class="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-2xs text-ink-500 dark:text-ink-400">
|
|
132
|
-
{owner && (
|
|
133
|
-
<a
|
|
134
|
-
href={repoHref}
|
|
135
|
-
target="_blank"
|
|
136
|
-
rel="noopener noreferrer"
|
|
137
|
-
class="relative z-10 font-mono hover:text-ink-900 dark:hover:text-ink-100"
|
|
138
|
-
>
|
|
139
|
-
<span class="text-ink-400 dark:text-ink-500">@</span>{owner}
|
|
140
|
-
{repo && <span class="text-ink-400 dark:text-ink-500">/{repo}</span>}
|
|
141
|
-
</a>
|
|
142
|
-
)}
|
|
143
|
-
{owner && <span class="text-ink-300 dark:text-ink-700">·</span>}
|
|
144
|
-
<span>{app.category}</span>
|
|
145
|
-
</div>
|
|
146
|
-
</div>
|
|
147
|
-
</header>
|
|
148
|
-
|
|
149
|
-
{/* Description */}
|
|
150
|
-
<p class="apps-row-desc m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-600 dark:text-ink-300">
|
|
151
|
-
{app.description}
|
|
152
|
-
</p>
|
|
153
|
-
|
|
154
|
-
{/* High-priority metadata row: stars · updated · license — the
|
|
155
|
-
"is this alive and usable?" answers at a glance. */}
|
|
156
|
-
<dl class="apps-row-meta grid grid-cols-3 gap-x-3 text-2xs">
|
|
157
|
-
{starsLabel !== null && (
|
|
158
|
-
<div class="flex items-baseline gap-1.5">
|
|
159
|
-
<dt class="sr-only">Stars</dt>
|
|
160
|
-
<dd class="inline-flex items-center gap-1 font-mono text-ink-700 dark:text-ink-300">
|
|
161
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor" class="text-amber-500">
|
|
162
|
-
<path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
|
|
163
|
-
</svg>
|
|
164
|
-
{starsLabel}
|
|
165
|
-
</dd>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
168
|
-
<div class="flex items-baseline gap-1.5">
|
|
169
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Updated</dt>
|
|
170
|
-
<dd class="font-mono text-ink-700 dark:text-ink-300" title={app.lastCommitAt ?? ""}>{updatedLabel}</dd>
|
|
171
|
-
</div>
|
|
172
|
-
<div class="flex items-baseline gap-1.5">
|
|
173
|
-
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">License</dt>
|
|
174
|
-
<dd class="font-mono text-ink-700 dark:text-ink-300">{app.license ?? "—"}</dd>
|
|
175
|
-
</div>
|
|
176
|
-
</dl>
|
|
177
|
-
|
|
178
|
-
{/* Medium-priority metadata: platforms + stacks. Hidden in compact mode
|
|
179
|
-
(controlled by parent via the `data-density` attribute on the
|
|
180
|
-
results container). */}
|
|
181
|
-
<div class="apps-row-chips flex flex-col gap-1.5">
|
|
182
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
183
|
-
{visiblePlatforms.map((p) => (
|
|
184
|
-
<span class="apps-chip inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300">
|
|
185
|
-
<Icon name={p} category="platform" size={11} />
|
|
186
|
-
<span>{p}</span>
|
|
187
|
-
</span>
|
|
188
|
-
))}
|
|
189
|
-
{platformOverflow > 0 && (
|
|
190
|
-
<span class="apps-chip text-2xs text-ink-400 dark:text-ink-500">+{platformOverflow}</span>
|
|
191
|
-
)}
|
|
192
|
-
</div>
|
|
193
|
-
<div class="flex flex-wrap items-center gap-1.5">
|
|
194
|
-
{visibleStacks.map((s) => (
|
|
195
|
-
<span class="apps-chip inline-flex items-center gap-1 rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300">
|
|
196
|
-
<Icon name={s} category="stack" size={11} />
|
|
197
|
-
<span>{s}</span>
|
|
198
|
-
</span>
|
|
199
|
-
))}
|
|
200
|
-
{stackOverflow > 0 && (
|
|
201
|
-
<span class="apps-chip text-2xs text-ink-400 dark:text-ink-500">+{stackOverflow}</span>
|
|
202
|
-
)}
|
|
203
|
-
</div>
|
|
204
|
-
</div>
|
|
205
|
-
|
|
206
|
-
{/* Footer actions: View details + GitHub. Always visible (even in
|
|
207
|
-
compact mode) so the two primary actions never disappear. */}
|
|
208
|
-
<footer class="relative z-10 mt-1 flex items-center gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
|
|
209
|
-
<a
|
|
210
|
-
href={detailHref}
|
|
211
|
-
class="relative z-10 inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
|
|
212
|
-
>
|
|
213
|
-
View details
|
|
214
|
-
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
215
|
-
<path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
|
|
216
|
-
</svg>
|
|
217
|
-
</a>
|
|
218
|
-
<a
|
|
219
|
-
href={repoHref}
|
|
220
|
-
target="_blank"
|
|
221
|
-
rel="noopener noreferrer"
|
|
222
|
-
class="relative z-10 inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
|
|
223
|
-
>
|
|
224
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
225
|
-
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z"/>
|
|
226
|
-
</svg>
|
|
227
|
-
GitHub
|
|
228
|
-
</a>
|
|
229
|
-
</footer>
|
|
230
|
-
</article>
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Pagination for the /apps list. Server-rendered; the page index in
|
|
4
|
-
* the URL is the source of truth. "Load more" is not used here — the
|
|
5
|
-
* full result set is small (a few hundred at most) so a real page
|
|
6
|
-
* index is simpler and more shareable.
|
|
7
|
-
*
|
|
8
|
-
* The component renders Prev / numbered pages / Next, plus a small
|
|
9
|
-
* "Page X of Y" caption. If there's only one page, returns nothing.
|
|
10
|
-
*/
|
|
11
|
-
interface Props {
|
|
12
|
-
/** 1-based current page. */
|
|
13
|
-
current: number;
|
|
14
|
-
/** Total page count. */
|
|
15
|
-
total: number;
|
|
16
|
-
/**
|
|
17
|
-
* Build the URL for a target page. Caller passes the function so
|
|
18
|
-
* we can preserve any active filter query string. The function
|
|
19
|
-
* receives the target page and should return a href with that
|
|
20
|
-
* page query param updated.
|
|
21
|
-
*/
|
|
22
|
-
hrefFor: (page: number) => string;
|
|
23
|
-
class?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const { current, total, hrefFor, class: className = "" } = Astro.props;
|
|
27
|
-
|
|
28
|
-
// Windowed page list: always show first, last, current ± 1. Hides
|
|
29
|
-
// distant pages with "…". Keeps the bar compact for large total.
|
|
30
|
-
function pageList(current: number, total: number): (number | "ellipsis")[] {
|
|
31
|
-
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
|
|
32
|
-
const out: (number | "ellipsis")[] = [];
|
|
33
|
-
const add = (n: number) => {
|
|
34
|
-
if (out[out.length - 1] !== n) out.push(n);
|
|
35
|
-
};
|
|
36
|
-
add(1);
|
|
37
|
-
if (current > 3) out.push("ellipsis");
|
|
38
|
-
for (let p = Math.max(2, current - 1); p <= Math.min(total - 1, current + 1); p++) add(p);
|
|
39
|
-
if (current < total - 2) out.push("ellipsis");
|
|
40
|
-
add(total);
|
|
41
|
-
return out;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const pages = pageList(current, total);
|
|
45
|
-
const hasPrev = current > 1;
|
|
46
|
-
const hasNext = current < total;
|
|
47
|
-
const prevHref = hasPrev ? hrefFor(current - 1) : null;
|
|
48
|
-
const nextHref = hasNext ? hrefFor(current + 1) : null;
|
|
49
|
-
|
|
50
|
-
const baseBtn =
|
|
51
|
-
"inline-flex h-8 min-w-8 items-center justify-center rounded-md border px-2 text-2xs font-medium transition-colors";
|
|
52
|
-
const inactiveBtn =
|
|
53
|
-
"border-ink-200 bg-white text-ink-700 hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60";
|
|
54
|
-
const activeBtn =
|
|
55
|
-
"border-ink-900 bg-ink-900 text-white dark:border-ink-100 dark:bg-ink-100 dark:text-ink-900";
|
|
56
|
-
const disabledBtn =
|
|
57
|
-
"border-ink-200 bg-white text-ink-300 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-700";
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
total > 1 && (
|
|
62
|
-
<nav
|
|
63
|
-
class={`mt-8 flex flex-col items-center gap-3 sm:flex-row sm:justify-between ${className}`}
|
|
64
|
-
aria-label="Pagination"
|
|
65
|
-
>
|
|
66
|
-
<p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
|
|
67
|
-
Page <span class="font-mono text-ink-700 dark:text-ink-300">{current}</span> of{" "}
|
|
68
|
-
<span class="font-mono text-ink-700 dark:text-ink-300">{total}</span>
|
|
69
|
-
</p>
|
|
70
|
-
<ul class="m-0 flex list-none flex-wrap items-center justify-center gap-1 p-0">
|
|
71
|
-
<li>
|
|
72
|
-
{hasPrev && prevHref ? (
|
|
73
|
-
<a
|
|
74
|
-
href={prevHref}
|
|
75
|
-
rel="prev"
|
|
76
|
-
class={`${baseBtn} ${inactiveBtn}`}
|
|
77
|
-
aria-label="Previous page"
|
|
78
|
-
>
|
|
79
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
80
|
-
<path d="M9.78 3.22a.75.75 0 0 1 0 1.06L6.56 7.5h6.69a.75.75 0 0 1 0 1.5H6.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z"/>
|
|
81
|
-
</svg>
|
|
82
|
-
<span class="ml-1">Prev</span>
|
|
83
|
-
</a>
|
|
84
|
-
) : (
|
|
85
|
-
<span class={`${baseBtn} ${disabledBtn} cursor-not-allowed`} aria-disabled="true">
|
|
86
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
87
|
-
<path d="M9.78 3.22a.75.75 0 0 1 0 1.06L6.56 7.5h6.69a.75.75 0 0 1 0 1.5H6.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z"/>
|
|
88
|
-
</svg>
|
|
89
|
-
<span class="ml-1">Prev</span>
|
|
90
|
-
</span>
|
|
91
|
-
)}
|
|
92
|
-
</li>
|
|
93
|
-
{pages.map((p) =>
|
|
94
|
-
p === "ellipsis" ? (
|
|
95
|
-
<li>
|
|
96
|
-
<span class={`${baseBtn} border-transparent text-ink-400 dark:text-ink-500`} aria-hidden="true">
|
|
97
|
-
…
|
|
98
|
-
</span>
|
|
99
|
-
</li>
|
|
100
|
-
) : p === current ? (
|
|
101
|
-
<li>
|
|
102
|
-
<span
|
|
103
|
-
class={`${baseBtn} ${activeBtn}`}
|
|
104
|
-
aria-current="page"
|
|
105
|
-
aria-label={`Page ${p}, current`}
|
|
106
|
-
>
|
|
107
|
-
{p}
|
|
108
|
-
</span>
|
|
109
|
-
</li>
|
|
110
|
-
) : (
|
|
111
|
-
<li>
|
|
112
|
-
<a
|
|
113
|
-
href={hrefFor(p)}
|
|
114
|
-
class={`${baseBtn} ${inactiveBtn}`}
|
|
115
|
-
aria-label={`Go to page ${p}`}
|
|
116
|
-
>
|
|
117
|
-
{p}
|
|
118
|
-
</a>
|
|
119
|
-
</li>
|
|
120
|
-
),
|
|
121
|
-
)}
|
|
122
|
-
<li>
|
|
123
|
-
{hasNext && nextHref ? (
|
|
124
|
-
<a
|
|
125
|
-
href={nextHref}
|
|
126
|
-
rel="next"
|
|
127
|
-
class={`${baseBtn} ${inactiveBtn}`}
|
|
128
|
-
aria-label="Next page"
|
|
129
|
-
>
|
|
130
|
-
<span class="mr-1">Next</span>
|
|
131
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
132
|
-
<path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06L9.44 8.5H2.75a.75.75 0 0 1 0-1.5h6.69L6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
|
|
133
|
-
</svg>
|
|
134
|
-
</a>
|
|
135
|
-
) : (
|
|
136
|
-
<span class={`${baseBtn} ${disabledBtn} cursor-not-allowed`} aria-disabled="true">
|
|
137
|
-
<span class="mr-1">Next</span>
|
|
138
|
-
<svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
|
|
139
|
-
<path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06L9.44 8.5H2.75a.75.75 0 0 1 0-1.5h6.69L6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
|
|
140
|
-
</svg>
|
|
141
|
-
</span>
|
|
142
|
-
)}
|
|
143
|
-
</li>
|
|
144
|
-
</ul>
|
|
145
|
-
</nav>
|
|
146
|
-
)
|
|
147
|
-
}
|