@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,219 @@
|
|
|
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 } 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
|
+
const label =
|
|
20
|
+
app.labels?.find((l) => l === "new" || l === "hot" || l === "mature") ??
|
|
21
|
+
app.labels?.[0];
|
|
22
|
+
|
|
23
|
+
const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
|
|
24
|
+
const visibleStacks = allStacks.slice(0, 5);
|
|
25
|
+
const stackOverflow = allStacks.length - visibleStacks.length;
|
|
26
|
+
|
|
27
|
+
const visiblePlatforms = app.platforms.slice(0, 5);
|
|
28
|
+
const platformOverflow = app.platforms.length - visiblePlatforms.length;
|
|
29
|
+
|
|
30
|
+
const detailHref = `/apps/${app.slug}`;
|
|
31
|
+
|
|
32
|
+
const isCurated = !!app.curation;
|
|
33
|
+
|
|
34
|
+
const projectTypeLabel = (() => {
|
|
35
|
+
switch (app.projectType) {
|
|
36
|
+
case "real-app": return "Real app";
|
|
37
|
+
case "production": return "Production";
|
|
38
|
+
case "reference": return "Reference";
|
|
39
|
+
case "demo": return "Demo";
|
|
40
|
+
case "template": return "Template";
|
|
41
|
+
default: return null;
|
|
42
|
+
}
|
|
43
|
+
})();
|
|
44
|
+
|
|
45
|
+
const starsLabel = formatStars(app.stars);
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<article
|
|
49
|
+
class="decision-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)]"
|
|
50
|
+
data-slug={app.slug}
|
|
51
|
+
>
|
|
52
|
+
{/* Avatar + name + badges */}
|
|
53
|
+
<header class="flex items-start gap-3">
|
|
54
|
+
<a
|
|
55
|
+
href={detailHref}
|
|
56
|
+
class="relative z-10 flex shrink-0"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
tabindex="-1"
|
|
59
|
+
>
|
|
60
|
+
{avatar ? (
|
|
61
|
+
<img
|
|
62
|
+
src={avatar}
|
|
63
|
+
alt={`${app.name} avatar`}
|
|
64
|
+
width="44"
|
|
65
|
+
height="44"
|
|
66
|
+
loading="lazy"
|
|
67
|
+
decoding="async"
|
|
68
|
+
class="h-11 w-11 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
|
|
69
|
+
/>
|
|
70
|
+
) : (
|
|
71
|
+
<span
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
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"
|
|
74
|
+
>
|
|
75
|
+
{app.name.slice(0, 2).toUpperCase()}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</a>
|
|
79
|
+
|
|
80
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
81
|
+
<div class="flex flex-wrap items-baseline gap-x-2">
|
|
82
|
+
<h3 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
83
|
+
<a
|
|
84
|
+
href={detailHref}
|
|
85
|
+
class="relative z-10 text-inherit no-underline hover:underline"
|
|
86
|
+
>
|
|
87
|
+
{app.name}
|
|
88
|
+
</a>
|
|
89
|
+
</h3>
|
|
90
|
+
{projectTypeLabel && (
|
|
91
|
+
<span class="inline-flex items-center rounded border border-ink-200 bg-ink-50 px-1.5 py-0 text-[10px] font-medium uppercase tracking-wider text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300">
|
|
92
|
+
{projectTypeLabel}
|
|
93
|
+
</span>
|
|
94
|
+
)}
|
|
95
|
+
{label && (
|
|
96
|
+
<span
|
|
97
|
+
class={`inline-flex items-center rounded border px-1.5 py-0 text-[10px] font-medium uppercase tracking-wider ${
|
|
98
|
+
label === "new"
|
|
99
|
+
? "border-emerald-300/60 bg-emerald-50 text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300"
|
|
100
|
+
: label === "hot"
|
|
101
|
+
? "border-orange-300/60 bg-orange-50 text-orange-800 dark:border-orange-800/60 dark:bg-orange-950/40 dark:text-orange-300"
|
|
102
|
+
: "border-blue-300/60 bg-blue-50 text-blue-800 dark:border-blue-800/60 dark:bg-blue-950/40 dark:text-blue-300"
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
{label}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
{isCurated && !app.curation?.reviewed && (
|
|
109
|
+
<span class="inline-flex items-center gap-1 rounded border border-amber-300/60 bg-amber-50 px-1.5 py-0 text-[10px] font-medium text-amber-800 dark:border-amber-800/60 dark:bg-amber-950/40 dark:text-amber-300">
|
|
110
|
+
<svg viewBox="0 0 16 16" width="9" height="9" aria-hidden="true" fill="currentColor">
|
|
111
|
+
<path d="M8 1.5a.75.75 0 0 1 .65.376l5.7 9.788A.75.75 0 0 1 13.7 12.75H2.3a.75.75 0 0 1-.65-1.086l5.7-9.788A.75.75 0 0 1 8 1.5ZM8 6a.75.75 0 0 1 .75.75v3a.75.75 0 0 1-1.5 0v-3A.75.75 0 0 1 8 6Zm0 6.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z"/>
|
|
112
|
+
</svg>
|
|
113
|
+
Auto-curated · Needs review
|
|
114
|
+
</span>
|
|
115
|
+
)}
|
|
116
|
+
{isCurated && app.curation?.reviewed && (
|
|
117
|
+
<span class="inline-flex items-center gap-1 rounded border border-emerald-300/60 bg-emerald-50 px-1.5 py-0 text-[10px] font-medium text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300">
|
|
118
|
+
<svg viewBox="0 0 16 16" width="9" height="9" aria-hidden="true" fill="currentColor">
|
|
119
|
+
<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"/>
|
|
120
|
+
</svg>
|
|
121
|
+
Curator reviewed
|
|
122
|
+
</span>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
<div class="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-2xs text-ink-500 dark:text-ink-400">
|
|
126
|
+
{owner && (
|
|
127
|
+
<a
|
|
128
|
+
href={app.repoUrl}
|
|
129
|
+
target="_blank"
|
|
130
|
+
rel="noopener noreferrer"
|
|
131
|
+
class="relative z-10 font-mono hover:text-ink-900 dark:hover:text-ink-100"
|
|
132
|
+
>
|
|
133
|
+
<span class="text-ink-400 dark:text-ink-500">@</span>{owner}
|
|
134
|
+
{repo && <span class="text-ink-400 dark:text-ink-500">/{repo}</span>}
|
|
135
|
+
</a>
|
|
136
|
+
)}
|
|
137
|
+
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
138
|
+
<span>{app.category}</span>
|
|
139
|
+
{app.status && (
|
|
140
|
+
<>
|
|
141
|
+
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
142
|
+
<span class="capitalize">{app.status}</span>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
{starsLabel && (
|
|
146
|
+
<>
|
|
147
|
+
<span class="text-ink-300 dark:text-ink-700">·</span>
|
|
148
|
+
<span class="inline-flex items-center gap-0.5 font-mono">
|
|
149
|
+
<svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
|
|
150
|
+
<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"/>
|
|
151
|
+
</svg>
|
|
152
|
+
{starsLabel}
|
|
153
|
+
</span>
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<a
|
|
160
|
+
href={detailHref}
|
|
161
|
+
class="relative z-10 hidden shrink-0 self-start text-2xs font-medium text-ink-500 hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
|
|
162
|
+
>
|
|
163
|
+
View details →
|
|
164
|
+
</a>
|
|
165
|
+
</header>
|
|
166
|
+
|
|
167
|
+
{/* One-line summary */}
|
|
168
|
+
<p class="m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-600 dark:text-ink-300">
|
|
169
|
+
{app.description}
|
|
170
|
+
</p>
|
|
171
|
+
|
|
172
|
+
{/* Metadata grid: stack, platforms, license — compact one-liner each */}
|
|
173
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-1 text-2xs sm:grid-cols-3">
|
|
174
|
+
<div class="flex items-baseline gap-1.5">
|
|
175
|
+
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Stack</dt>
|
|
176
|
+
<dd class="flex flex-wrap items-center gap-1 text-ink-700 dark:text-ink-300">
|
|
177
|
+
{visibleStacks.map((s) => (
|
|
178
|
+
<span class="inline-flex items-center gap-0.5">
|
|
179
|
+
<Icon name={s} category="stack" size={10} class="h-2.5 w-2.5" />
|
|
180
|
+
<span>{s}</span>
|
|
181
|
+
</span>
|
|
182
|
+
))}
|
|
183
|
+
{stackOverflow > 0 && <span class="text-ink-400 dark:text-ink-500">+{stackOverflow}</span>}
|
|
184
|
+
</dd>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="flex items-baseline gap-1.5">
|
|
187
|
+
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Platforms</dt>
|
|
188
|
+
<dd class="flex flex-wrap items-center gap-1 text-ink-700 dark:text-ink-300">
|
|
189
|
+
{visiblePlatforms.map((p) => (
|
|
190
|
+
<span class="inline-flex items-center gap-0.5">
|
|
191
|
+
<Icon name={p} category="platform" size={10} class="h-2.5 w-2.5" />
|
|
192
|
+
<span>{p}</span>
|
|
193
|
+
</span>
|
|
194
|
+
))}
|
|
195
|
+
{platformOverflow > 0 && <span class="text-ink-400 dark:text-ink-500">+{platformOverflow}</span>}
|
|
196
|
+
</dd>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="flex items-baseline gap-1.5">
|
|
199
|
+
<dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">License</dt>
|
|
200
|
+
<dd class="font-mono text-ink-700 dark:text-ink-300">
|
|
201
|
+
{app.license ?? "—"}
|
|
202
|
+
{app.codebaseSize && (
|
|
203
|
+
<>
|
|
204
|
+
<span class="ml-2 text-ink-300 dark:text-ink-700">·</span>
|
|
205
|
+
<span class="ml-1 text-ink-500 dark:text-ink-400">{app.codebaseSize}</span>
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
{app.difficulty && (
|
|
209
|
+
<>
|
|
210
|
+
<span class="ml-2 text-ink-300 dark:text-ink-700">·</span>
|
|
211
|
+
<span class="ml-1 text-ink-500 dark:text-ink-400">{app.difficulty}</span>
|
|
212
|
+
</>
|
|
213
|
+
)}
|
|
214
|
+
</dd>
|
|
215
|
+
</div>
|
|
216
|
+
</dl>
|
|
217
|
+
|
|
218
|
+
{/* Scores row — kept off the list to avoid bloat. See detail page. */}
|
|
219
|
+
</article>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Category } from "../data/categories";
|
|
3
|
+
import Container from "./layout/Container.astro";
|
|
4
|
+
import { countByCategory } from "../lib/taxonomy-counts";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
categories: Category[];
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { categories, class: className = "" } = Astro.props;
|
|
12
|
+
const counts = countByCategory();
|
|
13
|
+
|
|
14
|
+
// Sort by count desc, drop empty (already done in CategoryGrid, but
|
|
15
|
+
// re-apply here for safety in case the consumer rendered a raw list).
|
|
16
|
+
const visible = categories
|
|
17
|
+
.map((c) => ({ ...c, count: counts.get(c.name) ?? 0 }))
|
|
18
|
+
.filter((c) => c.count > 0)
|
|
19
|
+
.sort((a, b) => b.count - a.count)
|
|
20
|
+
.slice(0, 12);
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<section
|
|
24
|
+
id="explore-categories"
|
|
25
|
+
class={`section scroll-mt-20 border-t border-ink-200 dark:border-ink-800 ${className}`}
|
|
26
|
+
>
|
|
27
|
+
<Container>
|
|
28
|
+
<div class="flex flex-col gap-1.5">
|
|
29
|
+
<span
|
|
30
|
+
class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
31
|
+
>Explore</span
|
|
32
|
+
>
|
|
33
|
+
<h2
|
|
34
|
+
class="m-0 text-[1.5rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
35
|
+
>
|
|
36
|
+
Browse by category
|
|
37
|
+
</h2>
|
|
38
|
+
<p
|
|
39
|
+
class="m-0 text-[0.875rem] text-ink-500 dark:text-ink-400"
|
|
40
|
+
>
|
|
41
|
+
Jump straight to a category — links take you to a filtered list,
|
|
42
|
+
not a static browse page.
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<ul class="mt-5 grid list-none grid-cols-1 gap-2 p-0 sm:grid-cols-2 lg:grid-cols-3">
|
|
47
|
+
{
|
|
48
|
+
visible.map((c) => (
|
|
49
|
+
<li>
|
|
50
|
+
<a
|
|
51
|
+
href={`/apps?category=${encodeURIComponent(c.name)}`}
|
|
52
|
+
class="card card-hover group flex h-full items-start gap-3 p-3.5 no-underline"
|
|
53
|
+
>
|
|
54
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
55
|
+
<h3 class="m-0 text-[0.875rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
56
|
+
{c.name}
|
|
57
|
+
</h3>
|
|
58
|
+
<p class="m-0 truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
59
|
+
{c.blurb}
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
<span
|
|
63
|
+
class="shrink-0 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"
|
|
64
|
+
title={`${c.count} apps in this category`}
|
|
65
|
+
>
|
|
66
|
+
{c.count}
|
|
67
|
+
</span>
|
|
68
|
+
</a>
|
|
69
|
+
</li>
|
|
70
|
+
))
|
|
71
|
+
}
|
|
72
|
+
</ul>
|
|
73
|
+
</Container>
|
|
74
|
+
</section>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { Stack } from "../data/stacks";
|
|
3
|
+
import Container from "./layout/Container.astro";
|
|
4
|
+
import Icon from "./icons/Icon.astro";
|
|
5
|
+
import { countByStack } from "../lib/taxonomy-counts";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
stacks: Stack[];
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { stacks, class: className = "" } = Astro.props;
|
|
13
|
+
const counts = countByStack();
|
|
14
|
+
|
|
15
|
+
const statusLabel: Record<Stack["status"], string> = {
|
|
16
|
+
live: "Available now",
|
|
17
|
+
expanding: "Expanding",
|
|
18
|
+
planned: "Planned",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const statusBadge: Record<Stack["status"], string> = {
|
|
22
|
+
live: "border-emerald-300/60 bg-emerald-50 text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300",
|
|
23
|
+
expanding: "border-ink-200 bg-ink-50 text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300",
|
|
24
|
+
planned: "border-ink-200 bg-white text-ink-500 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-500",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const enriched = stacks
|
|
28
|
+
.map((s) => ({ ...s, count: counts.get(s.name) ?? 0 }))
|
|
29
|
+
.sort((a, b) => b.count - a.count);
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<section
|
|
33
|
+
id="explore-stacks"
|
|
34
|
+
class={`section scroll-mt-20 border-t border-ink-200 dark:border-ink-800 ${className}`}
|
|
35
|
+
>
|
|
36
|
+
<Container>
|
|
37
|
+
<div class="flex flex-col gap-1.5">
|
|
38
|
+
<span
|
|
39
|
+
class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
40
|
+
>Explore</span
|
|
41
|
+
>
|
|
42
|
+
<h2
|
|
43
|
+
class="m-0 text-[1.5rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
44
|
+
>
|
|
45
|
+
Browse by stack
|
|
46
|
+
</h2>
|
|
47
|
+
<p
|
|
48
|
+
class="m-0 text-[0.875rem] text-ink-500 dark:text-ink-400"
|
|
49
|
+
>
|
|
50
|
+
Pick a stack to see every curated app built with it. Live stacks
|
|
51
|
+
have apps right now; planned ones are on the roadmap.
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="mt-5 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
|
|
56
|
+
{
|
|
57
|
+
enriched.map((stack) => (
|
|
58
|
+
<a
|
|
59
|
+
href={`/apps?stack=${encodeURIComponent(stack.name)}`}
|
|
60
|
+
class="card card-hover group flex h-full items-center gap-3 p-3.5 no-underline"
|
|
61
|
+
>
|
|
62
|
+
<div
|
|
63
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-white dark:border-ink-800 dark:bg-ink-950"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
>
|
|
66
|
+
<Icon
|
|
67
|
+
name={stack.name}
|
|
68
|
+
category="stack"
|
|
69
|
+
size={18}
|
|
70
|
+
class="h-[18px] w-[18px]"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="flex min-w-0 flex-1 flex-col gap-0.5">
|
|
75
|
+
<div class="flex items-center gap-2">
|
|
76
|
+
<h3 class="m-0 text-[0.875rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
|
|
77
|
+
{stack.name}
|
|
78
|
+
</h3>
|
|
79
|
+
<span
|
|
80
|
+
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"
|
|
81
|
+
title={`${stack.count} apps in this stack`}
|
|
82
|
+
>
|
|
83
|
+
{stack.count} apps
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
<p class="m-0 truncate text-2xs text-ink-500 dark:text-ink-400">
|
|
87
|
+
{stack.blurb}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<span
|
|
92
|
+
class={`shrink-0 inline-flex items-center rounded-md border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider ${statusBadge[stack.status]}`}
|
|
93
|
+
aria-label={`Status: ${statusLabel[stack.status]}`}
|
|
94
|
+
>
|
|
95
|
+
{statusLabel[stack.status]}
|
|
96
|
+
</span>
|
|
97
|
+
</a>
|
|
98
|
+
))
|
|
99
|
+
}
|
|
100
|
+
</div>
|
|
101
|
+
</Container>
|
|
102
|
+
</section>
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { siteConfig } from "../data/config";
|
|
3
|
+
import Container from "./layout/Container.astro";
|
|
4
|
+
import { stats } from "../data/stats";
|
|
5
|
+
import { compact } from "../lib/format";
|
|
6
|
+
|
|
7
|
+
const githubUrl = stats.originalRepo;
|
|
8
|
+
|
|
9
|
+
const trustStats: { value: string; label: string }[] = [
|
|
10
|
+
{ value: compact(stats.apps), label: "apps indexed" },
|
|
11
|
+
{ value: String(stats.categories), label: "categories" },
|
|
12
|
+
{ value: String(stats.stacks), label: "stacks" },
|
|
13
|
+
{ value: compact(stats.contributors), label: "contributors" },
|
|
14
|
+
{ value: compact(stats.stars), label: "GitHub stars" },
|
|
15
|
+
];
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<section class="relative overflow-hidden border-b border-ink-200 dark:border-ink-800">
|
|
19
|
+
{/* Soft animated background gradient — subtle, not loud. */}
|
|
20
|
+
<div
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
class="pointer-events-none absolute inset-0 opacity-60 dark:opacity-40"
|
|
23
|
+
style="
|
|
24
|
+
background-image:
|
|
25
|
+
radial-gradient(60% 50% at 15% 20%, rgba(31, 111, 235, 0.12), transparent 60%),
|
|
26
|
+
radial-gradient(50% 40% at 85% 10%, rgba(110, 231, 183, 0.10), transparent 60%),
|
|
27
|
+
radial-gradient(40% 40% at 50% 90%, rgba(251, 146, 60, 0.08), transparent 60%);
|
|
28
|
+
"
|
|
29
|
+
></div>
|
|
30
|
+
|
|
31
|
+
<Container>
|
|
32
|
+
<div class="relative py-14 sm:py-20">
|
|
33
|
+
<div class="max-w-3xl">
|
|
34
|
+
<span
|
|
35
|
+
class="inline-flex animate-fade-in items-center gap-2 text-2xs font-medium text-ink-500 dark:text-ink-400"
|
|
36
|
+
>
|
|
37
|
+
<span
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"
|
|
40
|
+
></span>
|
|
41
|
+
A directory of real open-source apps
|
|
42
|
+
<span
|
|
43
|
+
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-700 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
44
|
+
aria-label="Pre-1.0 — data and UI are still being curated. Expect rough edges."
|
|
45
|
+
>
|
|
46
|
+
Beta
|
|
47
|
+
</span>
|
|
48
|
+
<span class="text-ink-400 dark:text-ink-500">·</span>
|
|
49
|
+
<span>v0.1</span>
|
|
50
|
+
</span>
|
|
51
|
+
|
|
52
|
+
<h1
|
|
53
|
+
class="mt-3 animate-fade-in-up text-[2.25rem] sm:text-[2.75rem] md:text-[3rem] font-semibold tracking-[-0.02em] leading-[1.05] text-ink-900 dark:text-ink-100 m-0"
|
|
54
|
+
style="animation-delay: 0.05s;"
|
|
55
|
+
>
|
|
56
|
+
Find open-source apps you can actually<br class="hidden sm:block" />
|
|
57
|
+
<span
|
|
58
|
+
class="bg-gradient-to-r from-ink-900 via-ink-700 to-ink-900 bg-clip-text text-transparent dark:from-ink-100 dark:via-ink-300 dark:to-ink-100"
|
|
59
|
+
style="background-size: 200% 200%; animation: fade-in-up 0.7s ease-out 0.1s both, gradient-shift 8s ease-in-out 1s infinite;"
|
|
60
|
+
>
|
|
61
|
+
run, study, and contribute to.
|
|
62
|
+
</span>
|
|
63
|
+
</h1>
|
|
64
|
+
|
|
65
|
+
<p
|
|
66
|
+
class="mt-4 animate-fade-in-up text-[0.9375rem] sm:text-[1rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0 max-w-[64ch]"
|
|
67
|
+
style="animation-delay: 0.15s;"
|
|
68
|
+
>
|
|
69
|
+
{siteConfig.name} is a curated directory of real applications with public source code.
|
|
70
|
+
Browse by stack, category, maturity, platform, license, stars, and activity — without
|
|
71
|
+
digging through random GitHub searches and outdated README lists.
|
|
72
|
+
</p>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
{/* Trust stats row — the single most credibility-critical block on the
|
|
76
|
+
page. Number + label, monospaced numbers so they line up. */}
|
|
77
|
+
<dl
|
|
78
|
+
class="mt-7 grid grid-cols-2 gap-x-4 gap-y-3 sm:grid-cols-5"
|
|
79
|
+
aria-label="Directory at a glance"
|
|
80
|
+
>
|
|
81
|
+
{
|
|
82
|
+
trustStats.map((s) => (
|
|
83
|
+
<div class="flex flex-col gap-0.5">
|
|
84
|
+
<dt class="sr-only">{s.label}</dt>
|
|
85
|
+
<dd class="m-0 font-mono text-[1.25rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
|
|
86
|
+
{s.value}
|
|
87
|
+
</dd>
|
|
88
|
+
<dd class="m-0 text-2xs uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
89
|
+
{s.label}
|
|
90
|
+
</dd>
|
|
91
|
+
</div>
|
|
92
|
+
))
|
|
93
|
+
}
|
|
94
|
+
</dl>
|
|
95
|
+
|
|
96
|
+
{/* Search + filters */}
|
|
97
|
+
<form
|
|
98
|
+
class="mt-7 flex flex-col gap-2 sm:flex-row sm:items-stretch"
|
|
99
|
+
role="search"
|
|
100
|
+
action="/apps"
|
|
101
|
+
method="get"
|
|
102
|
+
>
|
|
103
|
+
<label class="relative flex-1">
|
|
104
|
+
<span class="sr-only">Search apps, stacks, categories, licenses</span>
|
|
105
|
+
<svg
|
|
106
|
+
viewBox="0 0 16 16"
|
|
107
|
+
width="16"
|
|
108
|
+
height="16"
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
class="pointer-events-none absolute left-3.5 top-1/2 -translate-y-1/2 text-ink-400"
|
|
111
|
+
fill="currentColor"
|
|
112
|
+
>
|
|
113
|
+
<path
|
|
114
|
+
d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"
|
|
115
|
+
></path>
|
|
116
|
+
</svg>
|
|
117
|
+
<input
|
|
118
|
+
type="search"
|
|
119
|
+
name="q"
|
|
120
|
+
placeholder="Search apps, stacks, categories, licenses, platforms..."
|
|
121
|
+
class="h-10 w-full rounded-md border border-ink-200 bg-white pl-10 pr-3 text-[0.9375rem] text-ink-900 placeholder:text-ink-400 focus:border-ink-900 focus:outline-none focus:ring-2 focus:ring-ink-900/10 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-100 dark:placeholder:text-ink-500 dark:focus:border-ink-100 dark:focus:ring-ink-100/15"
|
|
122
|
+
/>
|
|
123
|
+
</label>
|
|
124
|
+
<a
|
|
125
|
+
href="/apps"
|
|
126
|
+
class="inline-flex h-10 items-center justify-center gap-1.5 rounded-md border border-ink-200 bg-white px-3 text-[0.875rem] 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/40"
|
|
127
|
+
aria-label="Browse all apps with filters"
|
|
128
|
+
>
|
|
129
|
+
<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true" fill="currentColor">
|
|
130
|
+
<path d="M.75 3h14.5a.75.75 0 0 1 .55 1.26l-5.55 6.1v4.39a.75.75 0 0 1-1.2.6l-2.5-1.87a.75.75 0 0 1-.3-.6v-2.52L.2 4.26A.75.75 0 0 1 .75 3Z"/>
|
|
131
|
+
</svg>
|
|
132
|
+
<span>Filters</span>
|
|
133
|
+
</a>
|
|
134
|
+
<button
|
|
135
|
+
type="submit"
|
|
136
|
+
class="btn-primary h-10 px-4 sm:px-5"
|
|
137
|
+
>
|
|
138
|
+
Search
|
|
139
|
+
</button>
|
|
140
|
+
</form>
|
|
141
|
+
|
|
142
|
+
<p
|
|
143
|
+
class="mt-2 text-2xs text-ink-500 dark:text-ink-400 m-0"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
>
|
|
146
|
+
Try{" "}
|
|
147
|
+
<a href="/apps?stack=Flutter" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Flutter apps</a>
|
|
148
|
+
<span class="mx-1">·</span>
|
|
149
|
+
<a href="/apps?category=Finance" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Finance</a>
|
|
150
|
+
<span class="mx-1">·</span>
|
|
151
|
+
<a href="/apps?label=hot" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Hot projects</a>
|
|
152
|
+
<span class="mx-1">·</span>
|
|
153
|
+
<a href="/apps?platform=Android" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Android</a>
|
|
154
|
+
<span class="mx-1">·</span>
|
|
155
|
+
<a href="/apps?label=mature" class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">Mature</a>
|
|
156
|
+
</p>
|
|
157
|
+
|
|
158
|
+
<div class="mt-5 flex flex-wrap items-center gap-2">
|
|
159
|
+
<a href="#new" class="btn-primary">Browse apps</a>
|
|
160
|
+
<a
|
|
161
|
+
href={`${githubUrl}/blob/main/CONTRIBUTING.md`}
|
|
162
|
+
target="_blank"
|
|
163
|
+
rel="noopener noreferrer"
|
|
164
|
+
class="btn-outline">Submit via GitHub</a
|
|
165
|
+
>
|
|
166
|
+
<a
|
|
167
|
+
href={githubUrl}
|
|
168
|
+
target="_blank"
|
|
169
|
+
rel="noopener noreferrer"
|
|
170
|
+
class="btn-ghost"
|
|
171
|
+
>View source on GitHub →</a
|
|
172
|
+
>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</Container>
|
|
176
|
+
</section>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { siteConfig } from "../data/config";
|
|
3
|
+
import Container from "./layout/Container.astro";
|
|
4
|
+
|
|
5
|
+
const points = [
|
|
6
|
+
{
|
|
7
|
+
title: "Real codebases",
|
|
8
|
+
body: "Not just tutorials or packages — apps that actually run.",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
title: "Searchable context",
|
|
12
|
+
body: "Find apps by stack, platform, category, license, and activity.",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
title: "Open data",
|
|
16
|
+
body: "Static site. GitHub-based data. Community maintained.",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<section id="about" class="section scroll-mt-20">
|
|
22
|
+
<Container size="narrow">
|
|
23
|
+
<span
|
|
24
|
+
class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400"
|
|
25
|
+
>About</span
|
|
26
|
+
>
|
|
27
|
+
<h2
|
|
28
|
+
class="mt-1 text-[1.5rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100 m-0"
|
|
29
|
+
>
|
|
30
|
+
Why this exists
|
|
31
|
+
</h2>
|
|
32
|
+
<p
|
|
33
|
+
class="mt-3 text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400 m-0"
|
|
34
|
+
>
|
|
35
|
+
Most app examples are either toy projects, abandoned lists, or hard to
|
|
36
|
+
search. {siteConfig.name} focuses on real application codebases that
|
|
37
|
+
developers can study, compare, and contribute to.
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
<ul
|
|
41
|
+
class="mt-6 grid list-none grid-cols-1 gap-3 p-0 m-0 sm:grid-cols-3"
|
|
42
|
+
>
|
|
43
|
+
{
|
|
44
|
+
points.map((p) => (
|
|
45
|
+
<li class="card p-4">
|
|
46
|
+
<h3 class="m-0 text-[0.875rem] font-semibold text-ink-900 dark:text-ink-100">
|
|
47
|
+
{p.title}
|
|
48
|
+
</h3>
|
|
49
|
+
<p class="mt-1 m-0 text-2xs leading-relaxed text-ink-500 dark:text-ink-400">
|
|
50
|
+
{p.body}
|
|
51
|
+
</p>
|
|
52
|
+
</li>
|
|
53
|
+
))
|
|
54
|
+
}
|
|
55
|
+
</ul>
|
|
56
|
+
</Container>
|
|
57
|
+
</section>
|