@grove-dev/astro 0.1.0
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 +81 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
- package/src/components/ActiveFilterChips.astro +20 -0
- package/src/components/CategoryGrid.astro +27 -0
- package/src/components/DirectoryFilters.astro +100 -0
- package/src/components/DirectoryHero.astro +32 -0
- package/src/components/HealthBadge.astro +12 -0
- package/src/components/ItemCard.astro +41 -0
- package/src/components/LensTabs.astro +26 -0
- package/src/components/MethodologyPanel.astro +20 -0
- package/src/components/Pagination.astro +24 -0
- package/src/components/ProjectDetail.astro +166 -0
- package/src/components/ScoreBars.astro +32 -0
- package/src/components/SubmitDraft.astro +137 -0
- package/src/index.ts +17 -0
- package/src/layouts/BaseLayout.astro +32 -0
- package/src/styles.css +675 -0
- package/templates/default/astro.config.mjs +23 -0
- package/templates/default/data/generated/apps.json +1 -0
- package/templates/default/data/generated/repo-stats.json +1 -0
- package/templates/default/package.json +35 -0
- package/templates/default/public/icons/platforms/android.svg +1 -0
- package/templates/default/public/icons/platforms/apple-dark.svg +1 -0
- package/templates/default/public/icons/platforms/apple-light.svg +1 -0
- package/templates/default/public/icons/platforms/chrome.svg +1 -0
- package/templates/default/public/icons/platforms/chromeos.svg +5 -0
- package/templates/default/public/icons/platforms/desktop.svg +4 -0
- package/templates/default/public/icons/platforms/embedded.svg +5 -0
- package/templates/default/public/icons/platforms/ios.svg +1 -0
- package/templates/default/public/icons/platforms/linux.svg +1 -0
- package/templates/default/public/icons/platforms/macos.svg +1 -0
- package/templates/default/public/icons/platforms/ubuntu.svg +1 -0
- package/templates/default/public/icons/platforms/web.svg +5 -0
- package/templates/default/public/icons/platforms/windows.svg +6 -0
- package/templates/default/public/icons/stacks/android.svg +1 -0
- package/templates/default/public/icons/stacks/apple-dark.svg +1 -0
- package/templates/default/public/icons/stacks/apple-light.svg +1 -0
- package/templates/default/public/icons/stacks/capacitor.svg +14 -0
- package/templates/default/public/icons/stacks/dart.svg +1 -0
- package/templates/default/public/icons/stacks/firebase.svg +1 -0
- package/templates/default/public/icons/stacks/flutter.svg +1 -0
- package/templates/default/public/icons/stacks/graphql.svg +1 -0
- package/templates/default/public/icons/stacks/ionic.svg +4 -0
- package/templates/default/public/icons/stacks/java.svg +1 -0
- package/templates/default/public/icons/stacks/javascript.svg +1 -0
- package/templates/default/public/icons/stacks/kotlin.svg +1 -0
- package/templates/default/public/icons/stacks/mongodb.svg +1 -0
- package/templates/default/public/icons/stacks/nodejs.svg +1 -0
- package/templates/default/public/icons/stacks/python.svg +1 -0
- package/templates/default/public/icons/stacks/react-native.svg +1 -0
- package/templates/default/public/icons/stacks/react.svg +1 -0
- package/templates/default/public/icons/stacks/rust.svg +1 -0
- package/templates/default/public/icons/stacks/solidity.svg +1 -0
- package/templates/default/public/icons/stacks/swift.svg +1 -0
- package/templates/default/public/icons/stacks/tensorflow.svg +1 -0
- package/templates/default/public/icons/stacks/typescript.svg +1 -0
- package/templates/default/public/og-image.svg +48 -0
- package/templates/default/public/robots.txt +53 -0
- package/templates/default/src/components/AppCard.astro +248 -0
- package/templates/default/src/components/AppSection.astro +58 -0
- package/templates/default/src/components/AppsIndexRow.astro +230 -0
- package/templates/default/src/components/AppsPagination.astro +147 -0
- package/templates/default/src/components/CategoryGrid.astro +80 -0
- package/templates/default/src/components/ContributorsGrid.astro +105 -0
- package/templates/default/src/components/DecisionRow.astro +219 -0
- package/templates/default/src/components/ExploreByCategory.astro +74 -0
- package/templates/default/src/components/ExploreByStack.astro +102 -0
- package/templates/default/src/components/Hero.astro +176 -0
- package/templates/default/src/components/MinimalAbout.astro +57 -0
- package/templates/default/src/components/OriginalCollection.astro +144 -0
- package/templates/default/src/components/RefinePanel.astro +272 -0
- package/templates/default/src/components/ScoreBars.astro +72 -0
- package/templates/default/src/components/SmartLensTabs.astro +65 -0
- package/templates/default/src/components/StackGrid.astro +95 -0
- package/templates/default/src/components/WhyThisExists.astro +81 -0
- package/templates/default/src/components/icons/Icon.astro +148 -0
- package/templates/default/src/components/layout/BaseLayout.astro +94 -0
- package/templates/default/src/components/layout/Container.astro +18 -0
- package/templates/default/src/components/layout/Footer.astro +145 -0
- package/templates/default/src/components/layout/Header.astro +144 -0
- package/templates/default/src/components/layout/SectionHeader.astro +42 -0
- package/templates/default/src/components/layout/Seo.astro +120 -0
- package/templates/default/src/components/layout/ThemeToggle.astro +97 -0
- package/templates/default/src/data/apps.ts +149 -0
- package/templates/default/src/data/categories.ts +27 -0
- package/templates/default/src/data/config.ts +28 -0
- package/templates/default/src/data/contributors.ts +96 -0
- package/templates/default/src/data/stacks.ts +47 -0
- package/templates/default/src/data/stats.ts +65 -0
- package/templates/default/src/data/types.ts +248 -0
- package/templates/default/src/lib/apps-search.ts +228 -0
- package/templates/default/src/lib/format.ts +36 -0
- package/templates/default/src/lib/lenses.ts +98 -0
- package/templates/default/src/lib/repo.ts +12 -0
- package/templates/default/src/lib/scores.ts +51 -0
- package/templates/default/src/lib/taxonomy-counts.ts +31 -0
- package/templates/default/src/pages/about.astro +433 -0
- package/templates/default/src/pages/apps/[slug].astro +709 -0
- package/templates/default/src/pages/apps/index.astro +669 -0
- package/templates/default/src/pages/contributors.astro +142 -0
- package/templates/default/src/pages/index.astro +66 -0
- package/templates/default/src/pages/sitemap.xml.ts +77 -0
- package/templates/default/src/pages/submit.astro +416 -0
- package/templates/default/src/styles/global.css +84 -0
- package/templates/default/tailwind.config.mjs +130 -0
|
@@ -0,0 +1,58 @@
|
|
|
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>
|
|
@@ -0,0 +1,230 @@
|
|
|
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>
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Category } from "../data/categories";
|
|
3
|
+
import SectionHeader from "./layout/SectionHeader.astro";
|
|
4
|
+
import Container from "./layout/Container.astro";
|
|
5
|
+
import { countByCategory } from "../lib/taxonomy-counts";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
categories: Category[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { categories } = Astro.props;
|
|
12
|
+
const counts = countByCategory();
|
|
13
|
+
|
|
14
|
+
// Hide categories that have zero apps in the directory. We don't lie about
|
|
15
|
+
// coverage. The taxonomy is a "future state" map; the rendered list is the
|
|
16
|
+
// intersection of the taxonomy and what actually exists in data/apps/.
|
|
17
|
+
const visible = categories
|
|
18
|
+
.map((c) => ({ ...c, count: counts.get(c.slug) ?? 0 }))
|
|
19
|
+
.filter((c) => c.count > 0)
|
|
20
|
+
.sort((a, b) => b.count - a.count);
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section
|
|
24
|
+
id="categories"
|
|
25
|
+
class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800"
|
|
26
|
+
>
|
|
27
|
+
<Container>
|
|
28
|
+
<SectionHeader
|
|
29
|
+
eyebrow="Categories"
|
|
30
|
+
title="Browse by category"
|
|
31
|
+
description="Find apps by what they do, not by which stack they use."
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
<div
|
|
35
|
+
class="overflow-hidden rounded-lg border border-ink-200 dark:border-ink-800"
|
|
36
|
+
>
|
|
37
|
+
<ul
|
|
38
|
+
class="m-0 grid list-none grid-cols-1 divide-y divide-ink-200 p-0 sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-3 dark:divide-ink-800"
|
|
39
|
+
>
|
|
40
|
+
{
|
|
41
|
+
visible.map((cat) => (
|
|
42
|
+
<li class="divide-x divide-ink-200 dark:divide-ink-800">
|
|
43
|
+
<a
|
|
44
|
+
href={`/apps?category=${encodeURIComponent(cat.name)}`}
|
|
45
|
+
class="group flex h-full items-start gap-3 p-4 no-underline transition-colors hover:bg-ink-50/60 dark:hover:bg-ink-900/40"
|
|
46
|
+
>
|
|
47
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
48
|
+
<div class="flex items-center gap-2">
|
|
49
|
+
<h3 class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
50
|
+
{cat.name}
|
|
51
|
+
</h3>
|
|
52
|
+
<span
|
|
53
|
+
class="inline-flex items-center rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ink-600 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-400"
|
|
54
|
+
title={`${cat.count} apps in this category`}
|
|
55
|
+
>
|
|
56
|
+
{cat.count} apps
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<p class="m-0 text-2xs leading-relaxed text-ink-500 dark:text-ink-400">
|
|
60
|
+
{cat.blurb}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<svg
|
|
64
|
+
viewBox="0 0 16 16"
|
|
65
|
+
width="12"
|
|
66
|
+
height="12"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
class="mt-1 shrink-0 text-ink-300 transition-colors group-hover:text-ink-500 dark:text-ink-700 dark:group-hover:text-ink-400"
|
|
69
|
+
fill="currentColor"
|
|
70
|
+
>
|
|
71
|
+
<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" />
|
|
72
|
+
</svg>
|
|
73
|
+
</a>
|
|
74
|
+
</li>
|
|
75
|
+
))
|
|
76
|
+
}
|
|
77
|
+
</ul>
|
|
78
|
+
</div>
|
|
79
|
+
</Container>
|
|
80
|
+
</section>
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Contributors grid for the home page. Renders all contributors at a
|
|
6
|
+
* uniform avatar size (no big/small distinction — the previous
|
|
7
|
+
* "top 10 + rest" layout implied a hierarchy that doesn't exist).
|
|
8
|
+
*
|
|
9
|
+
* When the contributor count is large enough that the homepage
|
|
10
|
+
* layout would feel cluttered, a "View all" link points to the
|
|
11
|
+
* dedicated `/contributors` page where the full list lives.
|
|
12
|
+
*/
|
|
13
|
+
import type { Contributor } from "../data/contributors";
|
|
14
|
+
import SectionHeader from "./layout/SectionHeader.astro";
|
|
15
|
+
import Container from "./layout/Container.astro";
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
contributors: Contributor[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { contributors } = Astro.props;
|
|
22
|
+
const total = contributors.length;
|
|
23
|
+
// Threshold above which we link out to the full /contributors page
|
|
24
|
+
// instead of inlining every avatar on the homepage.
|
|
25
|
+
const INLINE_LIMIT = 100;
|
|
26
|
+
const isCapped = total > INLINE_LIMIT;
|
|
27
|
+
const visible = isCapped ? contributors.slice(0, INLINE_LIMIT) : contributors;
|
|
28
|
+
const more = Math.max(0, total - INLINE_LIMIT);
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<section
|
|
32
|
+
id="contributors"
|
|
33
|
+
class="section scroll-mt-20 border-b border-ink-200 dark:border-ink-800"
|
|
34
|
+
>
|
|
35
|
+
<Container>
|
|
36
|
+
<SectionHeader
|
|
37
|
+
eyebrow="Community"
|
|
38
|
+
title="Thanks to every contributor."
|
|
39
|
+
description="This directory exists because people keep discovering, submitting, reviewing, and maintaining open-source app references."
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<ul
|
|
43
|
+
class="m-0 grid list-none grid-cols-[repeat(auto-fill,minmax(72px,1fr))] gap-x-3 gap-y-5 p-0"
|
|
44
|
+
>
|
|
45
|
+
{
|
|
46
|
+
visible.map((c) => (
|
|
47
|
+
<li class="flex flex-col items-center gap-1.5 text-center">
|
|
48
|
+
<a
|
|
49
|
+
href={c.profileUrl}
|
|
50
|
+
target="_blank"
|
|
51
|
+
rel="noopener noreferrer"
|
|
52
|
+
aria-label={`${c.name ?? c.username} on GitHub`}
|
|
53
|
+
title={`${c.name ?? c.username}${typeof c.contributions === "number" ? ` · ${c.contributions} contributions` : ""}`}
|
|
54
|
+
class="block transition-opacity hover:opacity-80"
|
|
55
|
+
>
|
|
56
|
+
<img
|
|
57
|
+
src={c.avatarUrl}
|
|
58
|
+
alt={`${c.name ?? c.username} avatar`}
|
|
59
|
+
loading="lazy"
|
|
60
|
+
width="40"
|
|
61
|
+
height="40"
|
|
62
|
+
class="h-10 w-10 rounded-full border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
63
|
+
/>
|
|
64
|
+
</a>
|
|
65
|
+
<a
|
|
66
|
+
href={c.profileUrl}
|
|
67
|
+
target="_blank"
|
|
68
|
+
rel="noopener noreferrer"
|
|
69
|
+
title={c.name ?? c.username}
|
|
70
|
+
class="block max-w-full truncate text-2xs text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
|
|
71
|
+
>
|
|
72
|
+
{c.username}
|
|
73
|
+
</a>
|
|
74
|
+
</li>
|
|
75
|
+
))
|
|
76
|
+
}
|
|
77
|
+
</ul>
|
|
78
|
+
|
|
79
|
+
<div
|
|
80
|
+
class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-2 text-2xs text-ink-500 dark:text-ink-400"
|
|
81
|
+
>
|
|
82
|
+
<span>
|
|
83
|
+
{isCapped
|
|
84
|
+
? <>Showing {visible.length} of {total} contributors on the homepage.</>
|
|
85
|
+
: <>Showing all {total} contributors.</>}
|
|
86
|
+
</span>
|
|
87
|
+
<a
|
|
88
|
+
href="/contributors"
|
|
89
|
+
class="inline-flex items-center gap-1 font-medium text-ink-700 hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
90
|
+
>
|
|
91
|
+
View all
|
|
92
|
+
{more > 0 ? ` (${more} more)` : ""} →
|
|
93
|
+
</a>
|
|
94
|
+
<span aria-hidden="true">·</span>
|
|
95
|
+
<a
|
|
96
|
+
href="https://github.com/tortuvshin/open-apps/graphs/contributors"
|
|
97
|
+
target="_blank"
|
|
98
|
+
rel="noopener noreferrer"
|
|
99
|
+
class="text-ink-700 hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
100
|
+
>
|
|
101
|
+
GitHub contributors graph
|
|
102
|
+
</a>
|
|
103
|
+
</div>
|
|
104
|
+
</Container>
|
|
105
|
+
</section>
|