@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.
Files changed (109) hide show
  1. package/README.md +81 -0
  2. package/dist/index.d.ts +26 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +26 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +58 -0
  7. package/src/components/ActiveFilterChips.astro +20 -0
  8. package/src/components/CategoryGrid.astro +27 -0
  9. package/src/components/DirectoryFilters.astro +100 -0
  10. package/src/components/DirectoryHero.astro +32 -0
  11. package/src/components/HealthBadge.astro +12 -0
  12. package/src/components/ItemCard.astro +41 -0
  13. package/src/components/LensTabs.astro +26 -0
  14. package/src/components/MethodologyPanel.astro +20 -0
  15. package/src/components/Pagination.astro +24 -0
  16. package/src/components/ProjectDetail.astro +166 -0
  17. package/src/components/ScoreBars.astro +32 -0
  18. package/src/components/SubmitDraft.astro +137 -0
  19. package/src/index.ts +17 -0
  20. package/src/layouts/BaseLayout.astro +32 -0
  21. package/src/styles.css +675 -0
  22. package/templates/default/astro.config.mjs +23 -0
  23. package/templates/default/data/generated/apps.json +1 -0
  24. package/templates/default/data/generated/repo-stats.json +1 -0
  25. package/templates/default/package.json +35 -0
  26. package/templates/default/public/icons/platforms/android.svg +1 -0
  27. package/templates/default/public/icons/platforms/apple-dark.svg +1 -0
  28. package/templates/default/public/icons/platforms/apple-light.svg +1 -0
  29. package/templates/default/public/icons/platforms/chrome.svg +1 -0
  30. package/templates/default/public/icons/platforms/chromeos.svg +5 -0
  31. package/templates/default/public/icons/platforms/desktop.svg +4 -0
  32. package/templates/default/public/icons/platforms/embedded.svg +5 -0
  33. package/templates/default/public/icons/platforms/ios.svg +1 -0
  34. package/templates/default/public/icons/platforms/linux.svg +1 -0
  35. package/templates/default/public/icons/platforms/macos.svg +1 -0
  36. package/templates/default/public/icons/platforms/ubuntu.svg +1 -0
  37. package/templates/default/public/icons/platforms/web.svg +5 -0
  38. package/templates/default/public/icons/platforms/windows.svg +6 -0
  39. package/templates/default/public/icons/stacks/android.svg +1 -0
  40. package/templates/default/public/icons/stacks/apple-dark.svg +1 -0
  41. package/templates/default/public/icons/stacks/apple-light.svg +1 -0
  42. package/templates/default/public/icons/stacks/capacitor.svg +14 -0
  43. package/templates/default/public/icons/stacks/dart.svg +1 -0
  44. package/templates/default/public/icons/stacks/firebase.svg +1 -0
  45. package/templates/default/public/icons/stacks/flutter.svg +1 -0
  46. package/templates/default/public/icons/stacks/graphql.svg +1 -0
  47. package/templates/default/public/icons/stacks/ionic.svg +4 -0
  48. package/templates/default/public/icons/stacks/java.svg +1 -0
  49. package/templates/default/public/icons/stacks/javascript.svg +1 -0
  50. package/templates/default/public/icons/stacks/kotlin.svg +1 -0
  51. package/templates/default/public/icons/stacks/mongodb.svg +1 -0
  52. package/templates/default/public/icons/stacks/nodejs.svg +1 -0
  53. package/templates/default/public/icons/stacks/python.svg +1 -0
  54. package/templates/default/public/icons/stacks/react-native.svg +1 -0
  55. package/templates/default/public/icons/stacks/react.svg +1 -0
  56. package/templates/default/public/icons/stacks/rust.svg +1 -0
  57. package/templates/default/public/icons/stacks/solidity.svg +1 -0
  58. package/templates/default/public/icons/stacks/swift.svg +1 -0
  59. package/templates/default/public/icons/stacks/tensorflow.svg +1 -0
  60. package/templates/default/public/icons/stacks/typescript.svg +1 -0
  61. package/templates/default/public/og-image.svg +48 -0
  62. package/templates/default/public/robots.txt +53 -0
  63. package/templates/default/src/components/AppCard.astro +248 -0
  64. package/templates/default/src/components/AppSection.astro +58 -0
  65. package/templates/default/src/components/AppsIndexRow.astro +230 -0
  66. package/templates/default/src/components/AppsPagination.astro +147 -0
  67. package/templates/default/src/components/CategoryGrid.astro +80 -0
  68. package/templates/default/src/components/ContributorsGrid.astro +105 -0
  69. package/templates/default/src/components/DecisionRow.astro +219 -0
  70. package/templates/default/src/components/ExploreByCategory.astro +74 -0
  71. package/templates/default/src/components/ExploreByStack.astro +102 -0
  72. package/templates/default/src/components/Hero.astro +176 -0
  73. package/templates/default/src/components/MinimalAbout.astro +57 -0
  74. package/templates/default/src/components/OriginalCollection.astro +144 -0
  75. package/templates/default/src/components/RefinePanel.astro +272 -0
  76. package/templates/default/src/components/ScoreBars.astro +72 -0
  77. package/templates/default/src/components/SmartLensTabs.astro +65 -0
  78. package/templates/default/src/components/StackGrid.astro +95 -0
  79. package/templates/default/src/components/WhyThisExists.astro +81 -0
  80. package/templates/default/src/components/icons/Icon.astro +148 -0
  81. package/templates/default/src/components/layout/BaseLayout.astro +94 -0
  82. package/templates/default/src/components/layout/Container.astro +18 -0
  83. package/templates/default/src/components/layout/Footer.astro +145 -0
  84. package/templates/default/src/components/layout/Header.astro +144 -0
  85. package/templates/default/src/components/layout/SectionHeader.astro +42 -0
  86. package/templates/default/src/components/layout/Seo.astro +120 -0
  87. package/templates/default/src/components/layout/ThemeToggle.astro +97 -0
  88. package/templates/default/src/data/apps.ts +149 -0
  89. package/templates/default/src/data/categories.ts +27 -0
  90. package/templates/default/src/data/config.ts +28 -0
  91. package/templates/default/src/data/contributors.ts +96 -0
  92. package/templates/default/src/data/stacks.ts +47 -0
  93. package/templates/default/src/data/stats.ts +65 -0
  94. package/templates/default/src/data/types.ts +248 -0
  95. package/templates/default/src/lib/apps-search.ts +228 -0
  96. package/templates/default/src/lib/format.ts +36 -0
  97. package/templates/default/src/lib/lenses.ts +98 -0
  98. package/templates/default/src/lib/repo.ts +12 -0
  99. package/templates/default/src/lib/scores.ts +51 -0
  100. package/templates/default/src/lib/taxonomy-counts.ts +31 -0
  101. package/templates/default/src/pages/about.astro +433 -0
  102. package/templates/default/src/pages/apps/[slug].astro +709 -0
  103. package/templates/default/src/pages/apps/index.astro +669 -0
  104. package/templates/default/src/pages/contributors.astro +142 -0
  105. package/templates/default/src/pages/index.astro +66 -0
  106. package/templates/default/src/pages/sitemap.xml.ts +77 -0
  107. package/templates/default/src/pages/submit.astro +416 -0
  108. package/templates/default/src/styles/global.css +84 -0
  109. package/templates/default/tailwind.config.mjs +130 -0
@@ -0,0 +1,120 @@
1
+ ---
2
+ import { siteConfig } from "../../data/config";
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ /**
6
+ * Per-page SEO: <title>, description, canonical, Open Graph, Twitter Card,
7
+ * and JSON-LD structured data. Designed to be used inside <head> by the
8
+ * BaseLayout. Pages pass the rich props they care about; everything else
9
+ * has a sensible default derived from the site config and the Astro URL.
10
+ *
11
+ * Renders:
12
+ * - <title>, meta description, canonical link
13
+ * - Open Graph (og:*) for Facebook, LinkedIn, Discord, Slack, etc.
14
+ * - Twitter Card (twitter:*) for X / Twitter
15
+ * - JSON-LD WebSite + Organization on every page
16
+ * - JSON-LD SoftwareSourceCode (or custom) when `jsonLd` is provided
17
+ * (e.g. on /apps/[slug] for the per-app structured data)
18
+ *
19
+ * Noindex is opt-in via the `noindex` prop. By default everything is
20
+ * indexable; the only noindex target today is the thin /submit wrapper.
21
+ */
22
+
23
+ interface Props {
24
+ title: string;
25
+ description: string;
26
+ /** Open Graph / Twitter image. Default: /og-image.svg. Should be absolute or root-relative. */
27
+ image?: string;
28
+ /** og:type. Default: "website". Use "article" for blog posts / changelog. */
29
+ type?: "website" | "article" | "profile";
30
+ /** Set true to add noindex,nofollow. */
31
+ noindex?: boolean;
32
+ /** Extra JSON-LD payload (object) — typically SoftwareSourceCode on app pages. */
33
+ jsonLd?: Record<string, unknown> | Record<string, unknown>[];
34
+ }
35
+
36
+ const {
37
+ title,
38
+ description,
39
+ image = "/og-image.svg",
40
+ type = "website",
41
+ noindex = false,
42
+ jsonLd,
43
+ } = Astro.props;
44
+
45
+ const site = Astro.site ?? new URL("https://open-apps.dev");
46
+ const canonical = new URL(Astro.url.pathname, site).toString();
47
+ const ogImage = new URL(image, site).toString();
48
+
49
+ // Truncate description for OG/Twitter where longer is worse (160–200 chars).
50
+ function truncate(s: string, max: number): string {
51
+ if (s.length <= max) return s;
52
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
53
+ }
54
+ const shortDescription = truncate(description, 200);
55
+
56
+ // WebSite + Organization JSON-LD. These are site-wide identity signals
57
+ // that should appear on every page (Google uses them for site links
58
+ // and the knowledge panel).
59
+ const websiteLd = {
60
+ "@context": "https://schema.org",
61
+ "@type": "WebSite",
62
+ name: `${siteConfig.name}`,
63
+ description:
64
+ "A curated, self-refreshing directory of real open-source application codebases.",
65
+ url: site.toString().replace(/\/$/, ""),
66
+ inLanguage: "en",
67
+ potentialAction: {
68
+ "@type": "SearchAction",
69
+ target: `${site.toString().replace(/\/$/, "")}/apps?q={search_term_string}`,
70
+ "query-input": "required name=search_term_string",
71
+ },
72
+ };
73
+
74
+ const organizationLd = {
75
+ "@context": "https://schema.org",
76
+ "@type": "Organization",
77
+ name: `${siteConfig.name}`,
78
+ url: site.toString().replace(/\/$/, ""),
79
+ logo: new URL("/og-image.svg", site).toString(),
80
+ sameAs: ["https://github.com/tortuvshin/open-apps"],
81
+ };
82
+ ---
83
+
84
+ <!-- Primary meta -->
85
+ <title>{title}</title>
86
+ <meta name="description" content={description} />
87
+ <link rel="canonical" href={canonical} />
88
+ {noindex && <meta name="robots" content="noindex,nofollow" />}
89
+
90
+ <!-- Open Graph -->
91
+ <meta property="og:type" content={type} />
92
+ <meta property="og:url" content={canonical} />
93
+ <meta property="og:title" content={title} />
94
+ <meta property="og:description" content={shortDescription} />
95
+ <meta property="og:image" content={ogImage} />
96
+ <meta property="og:image:width" content="1200" />
97
+ <meta property="og:image:height" content="630" />
98
+ <meta property="og:image:alt" content="{siteConfig.name} — a directory of real open-source apps" />
99
+ <meta property="og:site_name" content="{siteConfig.name}" />
100
+ <meta property="og:locale" content="en" />
101
+
102
+ <!-- Twitter Card -->
103
+ <meta name="twitter:card" content="summary_large_image" />
104
+ <meta name="twitter:title" content={title} />
105
+ <meta name="twitter:description" content={shortDescription} />
106
+ <meta name="twitter:image" content={ogImage} />
107
+ <meta name="twitter:image:alt" content="{siteConfig.name} — a directory of real open-source apps" />
108
+
109
+ <!-- JSON-LD: site-wide identity -->
110
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(websiteLd)} />
111
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(organizationLd)} />
112
+
113
+ <!-- JSON-LD: per-page (SoftwareSourceCode for /apps/[slug], etc.) -->
114
+ {jsonLd && (
115
+ <script
116
+ is:inline
117
+ type="application/ld+json"
118
+ set:html={JSON.stringify(Array.isArray(jsonLd) ? jsonLd : [jsonLd])}
119
+ />
120
+ )}
@@ -0,0 +1,97 @@
1
+ ---
2
+ // A three-position theme switcher: light / dark / system.
3
+ //
4
+ // State is persisted in `localStorage` under the key "open-apps-theme".
5
+ // On mount it reads the stored value (or "system" if none), applies
6
+ // the corresponding class to <html>, and re-syncs when the OS
7
+ // preference changes (only meaningful when mode is "system").
8
+ //
9
+ // Implementation note: the actual <html class="dark"> toggle happens
10
+ // in the inline script in `src/components/layout/BaseLayout.astro`
11
+ // (THEME_INIT). That script runs before paint so the user never sees
12
+ // a flash of the wrong theme.
13
+
14
+ interface Props {
15
+ class?: string;
16
+ }
17
+
18
+ const { class: className = "" } = Astro.props;
19
+ ---
20
+
21
+ <button
22
+ type="button"
23
+ id="theme-toggle"
24
+ data-theme-toggle
25
+ aria-label="Toggle theme"
26
+ title="Toggle theme"
27
+ class={`relative inline-flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-200 dark:hover:border-ink-700 dark:hover:bg-ink-900/40 ${className}`}
28
+ >
29
+ {/* Sun — shown in dark mode (click to go light) */}
30
+ <svg
31
+ class="h-4 w-4 hidden dark:block"
32
+ viewBox="0 0 24 24"
33
+ fill="none"
34
+ stroke="currentColor"
35
+ stroke-width="1.8"
36
+ stroke-linecap="round"
37
+ stroke-linejoin="round"
38
+ aria-hidden="true"
39
+ >
40
+ <circle cx="12" cy="12" r="4"/>
41
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
42
+ </svg>
43
+ {/* Moon — shown in light mode (click to go dark) */}
44
+ <svg
45
+ class="h-4 w-4 block dark:hidden"
46
+ viewBox="0 0 24 24"
47
+ fill="none"
48
+ stroke="currentColor"
49
+ stroke-width="1.8"
50
+ stroke-linecap="round"
51
+ stroke-linejoin="round"
52
+ aria-hidden="true"
53
+ >
54
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z"/>
55
+ </svg>
56
+ </button>
57
+
58
+ <script>
59
+ // The toggle button simply rotates between the three modes on
60
+ // click. The actual class application lives in the page-level
61
+ // THEME_INIT script so behaviour stays consistent.
62
+ const STORAGE_KEY = "open-apps-theme";
63
+ type Mode = "light" | "dark" | "system";
64
+
65
+ function currentMode(): Mode {
66
+ const stored = localStorage.getItem(STORAGE_KEY);
67
+ return stored === "light" || stored === "dark" || stored === "system"
68
+ ? stored
69
+ : "system";
70
+ }
71
+
72
+ function rotate(): Mode {
73
+ const order: Mode[] = ["light", "dark", "system"];
74
+ const next = order[(order.indexOf(currentMode()) + 1) % order.length];
75
+ localStorage.setItem(STORAGE_KEY, next);
76
+ // Notify the page-level listener.
77
+ window.dispatchEvent(new CustomEvent("theme:set", { detail: next }));
78
+ return next;
79
+ }
80
+
81
+ // A small tooltip label so the user knows what mode they'll get
82
+ // on the next click. We re-read current mode on focus/hover so it
83
+ // stays in sync.
84
+ function updateLabel(btn: HTMLButtonElement) {
85
+ btn.title = `Theme: ${currentMode()} (click to switch)`;
86
+ }
87
+
88
+ document.querySelectorAll<HTMLButtonElement>("[data-theme-toggle]").forEach((btn) => {
89
+ updateLabel(btn);
90
+ btn.addEventListener("click", () => {
91
+ rotate();
92
+ updateLabel(btn);
93
+ });
94
+ btn.addEventListener("mouseenter", () => updateLabel(btn));
95
+ btn.addEventListener("focus", () => updateLabel(btn));
96
+ });
97
+ </script>
@@ -0,0 +1,149 @@
1
+ import generatedJson from "../../data/generated/apps.json";
2
+ import type { OpenSourceApp, AppLabel } from "./types";
3
+
4
+ // ──────────────────────────────────────────────────────────────────────
5
+ // Source of truth: data/generated/apps.json, produced at build time
6
+ // from data/apps/*.yml by scripts/build-apps-json.mjs. The yml files
7
+ // are the human-edited source; this file is just a typed re-export
8
+ // that fills in safe defaults so consumers don't have to guard every
9
+ // optional field.
10
+ //
11
+ // When adding a new app: write a yml in data/apps/, run
12
+ // `pnpm run build:data`, and it will appear here.
13
+ // ──────────────────────────────────────────────────────────────────────
14
+
15
+ const generated = generatedJson as { apps: unknown[] };
16
+
17
+ type GeneratedApp = Partial<OpenSourceApp> & {
18
+ slug: string;
19
+ name: string;
20
+ repoUrl: string;
21
+ description?: string;
22
+ stack: string;
23
+ platforms: string[];
24
+ category: string;
25
+ activity?: {
26
+ stars?: number;
27
+ forks?: number;
28
+ lastCommitAt?: string | null;
29
+ contributors?: number;
30
+ updatedAt?: string;
31
+ };
32
+ };
33
+
34
+ /**
35
+ * Auto-compute labels for an app from its activity block. Used as a
36
+ * fallback when a curator hasn't hand-set `labels:` in the yml. Drives
37
+ * the homepage "new / hot / mature" sections.
38
+ *
39
+ * mature — stars ≥ 500, OR very high stars (popular, established)
40
+ * hot — last commit within the last 6 months (currently active)
41
+ * new — emerging: not mature, not in the last commit window, or
42
+ * no commit signal at all
43
+ *
44
+ * Multiple labels can apply (a popular active project is both mature
45
+ * and hot).
46
+ */
47
+ function computeLabels(g: GeneratedApp): AppLabel[] {
48
+ const labels: AppLabel[] = [];
49
+ // After the schemaVersion 1 migration, stars + lastCommitAt live at
50
+ // the top level of the generated record (the v1 normalizer in
51
+ // `app-schema.mjs` flattens `github.repository.stargazers_count`
52
+ // and `github.repository.pushed_at` to top-level). The old
53
+ // `g.activity.stars` shape still works for legacy records, so we
54
+ // fall back to it for mixed input.
55
+ const stars =
56
+ typeof g.stars === "number"
57
+ ? g.stars
58
+ : typeof g.activity?.stars === "number"
59
+ ? g.activity.stars
60
+ : 0;
61
+ const lastCommit = g.lastCommitAt ?? g.activity?.lastCommitAt;
62
+
63
+ if (stars >= 500) labels.push("mature");
64
+ if (stars >= 100 && stars < 500) labels.push("hot");
65
+ if (stars < 100) labels.push("new");
66
+
67
+ // Hot: recent commit and not already tagged as such
68
+ if (lastCommit) {
69
+ const sixMonthsAgo = new Date();
70
+ sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
71
+ if (new Date(lastCommit) >= sixMonthsAgo && !labels.includes("hot")) {
72
+ labels.push("hot");
73
+ }
74
+ }
75
+ return labels;
76
+ }
77
+
78
+ /**
79
+ * Map a generated yml-shaped record to the OpenSourceApp shape that
80
+ * pages expect. Fills defaults, flattens `activity.stars` → `stars`,
81
+ * and tolerates missing curation fields.
82
+ */
83
+ function normalize(g: GeneratedApp): OpenSourceApp {
84
+ const a = g.activity ?? {};
85
+ return {
86
+ slug: g.slug,
87
+ name: g.name,
88
+ description: g.description ?? "",
89
+ repoUrl: g.repoUrl,
90
+ homepageUrl: g.homepageUrl,
91
+ stack: g.stack,
92
+ stacks: g.stacks,
93
+ platforms: g.platforms ?? [],
94
+ distribution: g.distribution,
95
+ category: g.category,
96
+ tags: g.tags,
97
+ logoUrl: g.logoUrl,
98
+ stars: typeof a.stars === "number" ? a.stars : g.stars,
99
+ license: g.license,
100
+ status: g.status,
101
+ addedAt: g.addedAt,
102
+ lastCommitAt: a.lastCommitAt ?? g.lastCommitAt,
103
+ labels: g.labels && g.labels.length > 0 ? g.labels : computeLabels(g),
104
+ // Curation (optional)
105
+ projectType: g.projectType,
106
+ stateManagement: g.stateManagement,
107
+ backend: g.backend,
108
+ architecture: g.architecture,
109
+ difficulty: g.difficulty,
110
+ codebaseSize: g.codebaseSize,
111
+ bestFor: g.bestFor,
112
+ whyListed: g.whyListed,
113
+ caveats: g.caveats,
114
+ goodFirstIssues: g.goodFirstIssues,
115
+ contributionGuide: g.contributionGuide,
116
+ launchedBy: g.launchedBy,
117
+ launchAsk: g.launchAsk,
118
+ lenses: g.lenses,
119
+ scores: g.scores,
120
+ curation: g.curation,
121
+ github: g.github,
122
+ health: g.health,
123
+ tier: g.tier,
124
+ };
125
+ }
126
+
127
+ const generatedApps = generated.apps as GeneratedApp[];
128
+
129
+ export const apps: OpenSourceApp[] = generatedApps.map(normalize);
130
+
131
+ // ── Convenience filters ──────────────────────────────────────────────
132
+
133
+ export function newApps(): OpenSourceApp[] {
134
+ return apps.filter((a) => a.labels?.includes("new"));
135
+ }
136
+ export function hotApps(): OpenSourceApp[] {
137
+ return apps.filter((a) => a.labels?.includes("hot"));
138
+ }
139
+ export function matureApps(): OpenSourceApp[] {
140
+ return apps.filter((a) => a.labels?.includes("mature"));
141
+ }
142
+
143
+ // ── Lookups ──────────────────────────────────────────────────────────
144
+
145
+ const bySlug = new Map(apps.map((a) => [a.slug, a]));
146
+
147
+ export function appBySlug(slug: string): OpenSourceApp | undefined {
148
+ return bySlug.get(slug);
149
+ }
@@ -0,0 +1,27 @@
1
+ export type Category = {
2
+ slug: string;
3
+ name: string;
4
+ blurb: string;
5
+ };
6
+
7
+ // Static taxonomy. Counts are NOT stored here — they are computed at
8
+ // build time from data/generated/apps.json via src/lib/category-counts.ts
9
+ // so the numbers always match what's actually rendered.
10
+ export const categories: Category[] = [
11
+ { slug: "productivity", name: "Productivity", blurb: "Tasks, notes, calendars, knowledge bases." },
12
+ { slug: "finance", name: "Finance", blurb: "Budgeting, wallets, expense tracking." },
13
+ { slug: "education", name: "Education", blurb: "Flashcards, learning, reference apps." },
14
+ { slug: "tools", name: "Tools", blurb: "Browsers, utilities, dev helpers, file tools." },
15
+ { slug: "communication", name: "Communication", blurb: "Chat, messaging, social, team tools." },
16
+ { slug: "health-and-fitness", name: "Health and Fitness", blurb: "Workouts, nutrition, wellness tracking." },
17
+ { slug: "business", name: "Business", blurb: "Invoicing, jobs, marketplace, ops tools." },
18
+ { slug: "games", name: "Games", blurb: "Open-source games and engines." },
19
+ { slug: "media", name: "Media", blurb: "Music, movies, photos, audio and video." },
20
+ { slug: "entertainment", name: "Entertainment", blurb: "TV, movies, books, anime, ebooks." },
21
+ { slug: "social-network", name: "Social Network", blurb: "Decentralized social clients and readers." },
22
+ { slug: "shopping", name: "Shopping", blurb: "E-commerce, delivery, food, retail." },
23
+ { slug: "news", name: "News and Magazine", blurb: "Hacker News clients, RSS, magazines." },
24
+ { slug: "travel", name: "Travel", blurb: "Flights, tourism, cab sharing, guides." },
25
+ { slug: "lifestyle", name: "Lifestyle", blurb: "Habits, journaling, daily life tools." },
26
+ { slug: "personalization", name: "Personalization", blurb: "Launchers, themes, customization." },
27
+ ];
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Site config exposed to Astro pages.
3
+ *
4
+ * This file is the typed view of `curated.config.ts`. The CLI builds it
5
+ * from the project-root config at `grove build-data` time. Astro
6
+ * pages and components import it directly for branding strings.
7
+ *
8
+ * Do not edit by hand. Re-run `grove build-data` after changing
9
+ * `curated.config.ts`.
10
+ */
11
+
12
+ export type SiteConfig = {
13
+ name: string;
14
+ tagline: string;
15
+ description: string;
16
+ siteUrl: string;
17
+ repoUrl: string;
18
+ itemLabel: string;
19
+ };
20
+
21
+ export const siteConfig: SiteConfig = {
22
+ name: "Grove",
23
+ tagline: "A living, health-aware developer directory.",
24
+ description: "A searchable directory of open-source projects — organized by stack, category, platform, license, activity, and maturity.",
25
+ siteUrl: "https://example.com",
26
+ repoUrl: "https://github.com/tortuvshin/grove",
27
+ itemLabel: "project",
28
+ };
@@ -0,0 +1,96 @@
1
+ export type Contributor = {
2
+ username: string;
3
+ name?: string;
4
+ avatarUrl: string;
5
+ profileUrl: string;
6
+ contributions?: number;
7
+ };
8
+
9
+ // AUTO-GENERATED by scripts/sync-contributors.mjs — do not edit by hand.
10
+ // Re-run the script (or wait for the weekly workflow) to refresh from GitHub.
11
+ // Source: GET /repos/tortuvshin/open-apps/contributors (82 named contributors).
12
+ // Anonymous contributors are excluded — the homepage grid renders <a> avatars with no anonymous target.
13
+ export const contributors: Contributor[] = [
14
+ { username: "tortuvshin", avatarUrl: "https://avatars.githubusercontent.com/u/9257227?v=4", profileUrl: "https://github.com/tortuvshin", contributions: 140 },
15
+ { username: "j-j-gajjar", avatarUrl: "https://avatars.githubusercontent.com/u/57590688?v=4", profileUrl: "https://github.com/j-j-gajjar", contributions: 5 },
16
+ { username: "ArizArmeidi", avatarUrl: "https://avatars.githubusercontent.com/u/60814961?v=4", profileUrl: "https://github.com/ArizArmeidi", contributions: 3 },
17
+ { username: "vellt", avatarUrl: "https://avatars.githubusercontent.com/u/61885011?v=4", profileUrl: "https://github.com/vellt", contributions: 3 },
18
+ { username: "tejasbadone", avatarUrl: "https://avatars.githubusercontent.com/u/81501235?v=4", profileUrl: "https://github.com/tejasbadone", contributions: 3 },
19
+ { username: "ErfanRht", avatarUrl: "https://avatars.githubusercontent.com/u/68302442?v=4", profileUrl: "https://github.com/ErfanRht", contributions: 2 },
20
+ { username: "monolidth", avatarUrl: "https://avatars.githubusercontent.com/u/71930307?v=4", profileUrl: "https://github.com/monolidth", contributions: 2 },
21
+ { username: "proninyaroslav", avatarUrl: "https://avatars.githubusercontent.com/u/7840559?v=4", profileUrl: "https://github.com/proninyaroslav", contributions: 2 },
22
+ { username: "Tarikul711", avatarUrl: "https://avatars.githubusercontent.com/u/9304805?v=4", profileUrl: "https://github.com/Tarikul711", contributions: 2 },
23
+ { username: "lucalves", avatarUrl: "https://avatars.githubusercontent.com/u/17712401?v=4", profileUrl: "https://github.com/lucalves", contributions: 2 },
24
+ { username: "1hanzla100", avatarUrl: "https://avatars.githubusercontent.com/u/59178380?v=4", profileUrl: "https://github.com/1hanzla100", contributions: 2 },
25
+ { username: "jerichoi224", avatarUrl: "https://avatars.githubusercontent.com/u/4184001?v=4", profileUrl: "https://github.com/jerichoi224", contributions: 2 },
26
+ { username: "tsvillain", avatarUrl: "https://avatars.githubusercontent.com/u/42140007?v=4", profileUrl: "https://github.com/tsvillain", contributions: 1 },
27
+ { username: "TNT-Likely", avatarUrl: "https://avatars.githubusercontent.com/u/9278923?v=4", profileUrl: "https://github.com/TNT-Likely", contributions: 1 },
28
+ { username: "sumittiware", avatarUrl: "https://avatars.githubusercontent.com/u/64718675?v=4", profileUrl: "https://github.com/sumittiware", contributions: 1 },
29
+ { username: "simonbengtsson", avatarUrl: "https://avatars.githubusercontent.com/u/3586691?v=4", profileUrl: "https://github.com/simonbengtsson", contributions: 1 },
30
+ { username: "shaxxx", avatarUrl: "https://avatars.githubusercontent.com/u/1778875?v=4", profileUrl: "https://github.com/shaxxx", contributions: 1 },
31
+ { username: "88srzh", avatarUrl: "https://avatars.githubusercontent.com/u/61797666?v=4", profileUrl: "https://github.com/88srzh", contributions: 1 },
32
+ { username: "SatyamX64", avatarUrl: "https://avatars.githubusercontent.com/u/62766656?v=4", profileUrl: "https://github.com/SatyamX64", contributions: 1 },
33
+ { username: "sarvesh4396", avatarUrl: "https://avatars.githubusercontent.com/u/68162479?v=4", profileUrl: "https://github.com/sarvesh4396", contributions: 1 },
34
+ { username: "illussna", avatarUrl: "https://avatars.githubusercontent.com/u/20238269?v=4", profileUrl: "https://github.com/illussna", contributions: 1 },
35
+ { username: "SAHILDUDHAL21", avatarUrl: "https://avatars.githubusercontent.com/u/129080375?v=4", profileUrl: "https://github.com/SAHILDUDHAL21", contributions: 1 },
36
+ { username: "SaadAAkash", avatarUrl: "https://avatars.githubusercontent.com/u/16515550?v=4", profileUrl: "https://github.com/SaadAAkash", contributions: 1 },
37
+ { username: "Rohithgilla12", avatarUrl: "https://avatars.githubusercontent.com/u/19389850?v=4", profileUrl: "https://github.com/Rohithgilla12", contributions: 1 },
38
+ { username: "teolemon", avatarUrl: "https://avatars.githubusercontent.com/u/1689815?v=4", profileUrl: "https://github.com/teolemon", contributions: 1 },
39
+ { username: "Re4ch-Jay", avatarUrl: "https://avatars.githubusercontent.com/u/111555521?v=4", profileUrl: "https://github.com/Re4ch-Jay", contributions: 1 },
40
+ { username: "pauek", avatarUrl: "https://avatars.githubusercontent.com/u/4069?v=4", profileUrl: "https://github.com/pauek", contributions: 1 },
41
+ { username: "DaNnyKg07", avatarUrl: "https://avatars.githubusercontent.com/u/78636435?v=4", profileUrl: "https://github.com/DaNnyKg07", contributions: 1 },
42
+ { username: "omar-h-omar", avatarUrl: "https://avatars.githubusercontent.com/u/58376513?v=4", profileUrl: "https://github.com/omar-h-omar", contributions: 1 },
43
+ { username: "FatoomRe", avatarUrl: "https://avatars.githubusercontent.com/u/112693194?v=4", profileUrl: "https://github.com/FatoomRe", contributions: 1 },
44
+ { username: "yon30c", avatarUrl: "https://avatars.githubusercontent.com/u/83658386?v=4", profileUrl: "https://github.com/yon30c", contributions: 1 },
45
+ { username: "y-pakorn", avatarUrl: "https://avatars.githubusercontent.com/u/48324733?v=4", profileUrl: "https://github.com/y-pakorn", contributions: 1 },
46
+ { username: "virlow-voice", avatarUrl: "https://avatars.githubusercontent.com/u/102702984?v=4", profileUrl: "https://github.com/virlow-voice", contributions: 1 },
47
+ { username: "trizin", avatarUrl: "https://avatars.githubusercontent.com/u/25263018?v=4", profileUrl: "https://github.com/trizin", contributions: 1 },
48
+ { username: "sparkleMing", avatarUrl: "https://avatars.githubusercontent.com/u/38350996?v=4", profileUrl: "https://github.com/sparkleMing", contributions: 1 },
49
+ { username: "softjapan", avatarUrl: "https://avatars.githubusercontent.com/u/11269856?v=4", profileUrl: "https://github.com/softjapan", contributions: 1 },
50
+ { username: "moda20", avatarUrl: "https://avatars.githubusercontent.com/u/23613485?v=4", profileUrl: "https://github.com/moda20", contributions: 1 },
51
+ { username: "mihalycsaba", avatarUrl: "https://avatars.githubusercontent.com/u/50019470?v=4", profileUrl: "https://github.com/mihalycsaba", contributions: 1 },
52
+ { username: "ivirtex", avatarUrl: "https://avatars.githubusercontent.com/u/57679865?v=4", profileUrl: "https://github.com/ivirtex", contributions: 1 },
53
+ { username: "comerc", avatarUrl: "https://avatars.githubusercontent.com/u/1025241?v=4", profileUrl: "https://github.com/comerc", contributions: 1 },
54
+ { username: "albemala", avatarUrl: "https://avatars.githubusercontent.com/u/2332860?v=4", profileUrl: "https://github.com/albemala", contributions: 1 },
55
+ { username: "xal", avatarUrl: "https://avatars.githubusercontent.com/u/1388084?v=4", profileUrl: "https://github.com/xal", contributions: 1 },
56
+ { username: "yash1200", avatarUrl: "https://avatars.githubusercontent.com/u/38391570?v=4", profileUrl: "https://github.com/yash1200", contributions: 1 },
57
+ { username: "wad3g", avatarUrl: "https://avatars.githubusercontent.com/u/2086896?v=4", profileUrl: "https://github.com/wad3g", contributions: 1 },
58
+ { username: "VarunS2002", avatarUrl: "https://avatars.githubusercontent.com/u/30069380?v=4", profileUrl: "https://github.com/VarunS2002", contributions: 1 },
59
+ { username: "friebetill", avatarUrl: "https://avatars.githubusercontent.com/u/10923085?v=4", profileUrl: "https://github.com/friebetill", contributions: 1 },
60
+ { username: "theachoem", avatarUrl: "https://avatars.githubusercontent.com/u/29684683?v=4", profileUrl: "https://github.com/theachoem", contributions: 1 },
61
+ { username: "Enhancifire", avatarUrl: "https://avatars.githubusercontent.com/u/81345756?v=4", profileUrl: "https://github.com/Enhancifire", contributions: 1 },
62
+ { username: "Ehtiram-Ullah", avatarUrl: "https://avatars.githubusercontent.com/u/157794767?v=4", profileUrl: "https://github.com/Ehtiram-Ullah", contributions: 1 },
63
+ { username: "Deepanshu-Rohilla", avatarUrl: "https://avatars.githubusercontent.com/u/52520071?v=4", profileUrl: "https://github.com/Deepanshu-Rohilla", contributions: 1 },
64
+ { username: "DattatreyaReddy", avatarUrl: "https://avatars.githubusercontent.com/u/58727124?v=4", profileUrl: "https://github.com/DattatreyaReddy", contributions: 1 },
65
+ { username: "shurygindv", avatarUrl: "https://avatars.githubusercontent.com/u/38868386?v=4", profileUrl: "https://github.com/shurygindv", contributions: 1 },
66
+ { username: "csuka1219", avatarUrl: "https://avatars.githubusercontent.com/u/70962953?v=4", profileUrl: "https://github.com/csuka1219", contributions: 1 },
67
+ { username: "ChangJoo-Park", avatarUrl: "https://avatars.githubusercontent.com/u/1451365?v=4", profileUrl: "https://github.com/ChangJoo-Park", contributions: 1 },
68
+ { username: "KyleKun", avatarUrl: "https://avatars.githubusercontent.com/u/36611739?v=4", profileUrl: "https://github.com/KyleKun", contributions: 1 },
69
+ { username: "bipinct", avatarUrl: "https://avatars.githubusercontent.com/u/1249142?v=4", profileUrl: "https://github.com/bipinct", contributions: 1 },
70
+ { username: "Spyy004", avatarUrl: "https://avatars.githubusercontent.com/u/54628130?v=4", profileUrl: "https://github.com/Spyy004", contributions: 1 },
71
+ { username: "AyanDas-99", avatarUrl: "https://avatars.githubusercontent.com/u/86493736?v=4", profileUrl: "https://github.com/AyanDas-99", contributions: 1 },
72
+ { username: "aryamangodara", avatarUrl: "https://avatars.githubusercontent.com/u/77824849?v=4", profileUrl: "https://github.com/aryamangodara", contributions: 1 },
73
+ { username: "agordn52", avatarUrl: "https://avatars.githubusercontent.com/u/17294994?v=4", profileUrl: "https://github.com/agordn52", contributions: 1 },
74
+ { username: "akmadan", avatarUrl: "https://avatars.githubusercontent.com/u/56507451?v=4", profileUrl: "https://github.com/akmadan", contributions: 1 },
75
+ { username: "akashlilhare", avatarUrl: "https://avatars.githubusercontent.com/u/39927872?v=4", profileUrl: "https://github.com/akashlilhare", contributions: 1 },
76
+ { username: "aditya-njr", avatarUrl: "https://avatars.githubusercontent.com/u/169588760?v=4", profileUrl: "https://github.com/aditya-njr", contributions: 1 },
77
+ { username: "abhith", avatarUrl: "https://avatars.githubusercontent.com/u/7039457?v=4", profileUrl: "https://github.com/abhith", contributions: 1 },
78
+ { username: "NaserElziadna", avatarUrl: "https://avatars.githubusercontent.com/u/24838963?v=4", profileUrl: "https://github.com/NaserElziadna", contributions: 1 },
79
+ { username: "brownboycodes", avatarUrl: "https://avatars.githubusercontent.com/u/64550304?v=4", profileUrl: "https://github.com/brownboycodes", contributions: 1 },
80
+ { username: "mzsdev1", avatarUrl: "https://avatars.githubusercontent.com/u/34914546?v=4", profileUrl: "https://github.com/mzsdev1", contributions: 1 },
81
+ { username: "mllrr96", avatarUrl: "https://avatars.githubusercontent.com/u/28247455?v=4", profileUrl: "https://github.com/mllrr96", contributions: 1 },
82
+ { username: "hadiuzzaman524", avatarUrl: "https://avatars.githubusercontent.com/u/52348628?v=4", profileUrl: "https://github.com/hadiuzzaman524", contributions: 1 },
83
+ { username: "deandreamatias", avatarUrl: "https://avatars.githubusercontent.com/u/21011641?v=4", profileUrl: "https://github.com/deandreamatias", contributions: 1 },
84
+ { username: "mateusz-bak", avatarUrl: "https://avatars.githubusercontent.com/u/32651935?v=4", profileUrl: "https://github.com/mateusz-bak", contributions: 1 },
85
+ { username: "maksymilian-majer", avatarUrl: "https://avatars.githubusercontent.com/u/1215868?v=4", profileUrl: "https://github.com/maksymilian-majer", contributions: 1 },
86
+ { username: "musfique113", avatarUrl: "https://avatars.githubusercontent.com/u/53111065?v=4", profileUrl: "https://github.com/musfique113", contributions: 1 },
87
+ { username: "Mufaddal5253110", avatarUrl: "https://avatars.githubusercontent.com/u/60619133?v=4", profileUrl: "https://github.com/Mufaddal5253110", contributions: 1 },
88
+ { username: "Kessir", avatarUrl: "https://avatars.githubusercontent.com/u/2332730?v=4", profileUrl: "https://github.com/Kessir", contributions: 1 },
89
+ { username: "kaiwalyakoparkar", avatarUrl: "https://avatars.githubusercontent.com/u/53310847?v=4", profileUrl: "https://github.com/kaiwalyakoparkar", contributions: 1 },
90
+ { username: "Livinglist", avatarUrl: "https://avatars.githubusercontent.com/u/7277662?v=4", profileUrl: "https://github.com/Livinglist", contributions: 1 },
91
+ { username: "jerald-jacob", avatarUrl: "https://avatars.githubusercontent.com/u/25875900?v=4", profileUrl: "https://github.com/jerald-jacob", contributions: 1 },
92
+ { username: "jaysavsani07", avatarUrl: "https://avatars.githubusercontent.com/u/43170852?v=4", profileUrl: "https://github.com/jaysavsani07", contributions: 1 },
93
+ { username: "javipacheco", avatarUrl: "https://avatars.githubusercontent.com/u/456025?v=4", profileUrl: "https://github.com/javipacheco", contributions: 1 },
94
+ { username: "7flash", avatarUrl: "https://avatars.githubusercontent.com/u/4569866?v=4", profileUrl: "https://github.com/7flash", contributions: 1 },
95
+ { username: "floranguyen0", avatarUrl: "https://avatars.githubusercontent.com/u/72301141?v=4", profileUrl: "https://github.com/floranguyen0", contributions: 1 },
96
+ ];
@@ -0,0 +1,47 @@
1
+ export type Stack = {
2
+ slug: string;
3
+ name: string;
4
+ blurb: string;
5
+ status: "live" | "expanding" | "planned";
6
+ };
7
+
8
+ // Static taxonomy. Counts are NOT stored here — they are computed at
9
+ // build time from data/generated/apps.json. See src/lib/stack-counts.ts.
10
+ export const stacks: Stack[] = [
11
+ {
12
+ slug: "flutter",
13
+ name: "Flutter",
14
+ blurb: "Dart-based, cross-platform mobile apps.",
15
+ status: "live",
16
+ },
17
+ {
18
+ slug: "react-native",
19
+ name: "React Native",
20
+ blurb: "JavaScript / TypeScript mobile apps using React.",
21
+ status: "expanding",
22
+ },
23
+ {
24
+ slug: "ios",
25
+ name: "iOS",
26
+ blurb: "Native Swift apps from the App Store and the open-source community.",
27
+ status: "expanding",
28
+ },
29
+ {
30
+ slug: "android",
31
+ name: "Android",
32
+ blurb: "Native Kotlin / Java apps with a long, mature history.",
33
+ status: "expanding",
34
+ },
35
+ {
36
+ slug: "kmp",
37
+ name: "Kotlin Multiplatform",
38
+ blurb: "Shared Kotlin codebases targeting iOS and Android.",
39
+ status: "planned",
40
+ },
41
+ {
42
+ slug: "capacitor",
43
+ name: "Capacitor",
44
+ blurb: "Hybrid mobile apps built on web standards.",
45
+ status: "planned",
46
+ },
47
+ ];
@@ -0,0 +1,65 @@
1
+ // Site stats, derived from generated JSON files at build time.
2
+ //
3
+ // This file imports the same JSON the rest of the app uses, so the numbers
4
+ // here always match what's actually rendered. No hand-tuned magic numbers.
5
+ //
6
+ // Numbers in the rendered UI (hero, header, OriginalCollection) come from
7
+ // this module. If a number looks wrong, refresh the source data:
8
+ // - pnpm run sync:repo-stats → data/generated/repo-stats.json (open-apps repo)
9
+ // - pnpm run sync:contributors → src/data/contributors.ts
10
+ // - pnpm run refresh:activity → data/apps/*.yml (per-app activity, then build:data)
11
+ import generatedJson from "../../data/generated/apps.json";
12
+ import repoStats from "../../data/generated/repo-stats.json";
13
+ import { categories } from "./categories";
14
+ import { contributors } from "./contributors";
15
+
16
+ type GeneratedApp = {
17
+ stack?: string;
18
+ platforms?: string[];
19
+ };
20
+
21
+ type RepoStats = {
22
+ stars: number;
23
+ forks: number;
24
+ watchers?: number;
25
+ openIssues?: number;
26
+ defaultBranch?: string;
27
+ pushedAt?: string;
28
+ syncedAt?: string;
29
+ };
30
+
31
+ const raw = generatedJson as { apps?: GeneratedApp[] };
32
+ const apps: GeneratedApp[] = raw.apps ?? [];
33
+ const repo = repoStats as RepoStats;
34
+
35
+ const platforms = new Set<string>();
36
+ for (const a of apps) {
37
+ for (const p of a.platforms ?? []) platforms.add(p);
38
+ }
39
+
40
+ export type SiteStats = {
41
+ apps: number;
42
+ contributors: number;
43
+ stars: number;
44
+ forks: number;
45
+ categories: number;
46
+ stacks: number;
47
+ platforms: number;
48
+ originalRepo: string;
49
+ };
50
+
51
+ export const stats: SiteStats = {
52
+ apps: apps.length,
53
+ contributors: contributors.length,
54
+ // Stars/forks are the open-apps repo's own — sourced from
55
+ // data/generated/repo-stats.json, refreshed weekly by the
56
+ // sync-repo-stats workflow. Previously these were summed across
57
+ // every app in the directory, which produced 6-figure numbers
58
+ // for a 4k-star repo.
59
+ stars: repo.stars,
60
+ forks: repo.forks,
61
+ categories: categories.length,
62
+ stacks: new Set(apps.map((a) => a.stack).filter(Boolean)).size,
63
+ platforms: platforms.size,
64
+ originalRepo: "https://github.com/tortuvshin/open-apps",
65
+ };