@grove-dev/astro 0.2.6 → 0.2.13

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 (136) hide show
  1. package/dist/lib/display.d.ts +11 -17
  2. package/dist/lib/display.d.ts.map +1 -1
  3. package/dist/lib/display.js +43 -43
  4. package/dist/lib/display.js.map +1 -1
  5. package/dist/lib/format.d.ts +6 -7
  6. package/dist/lib/format.d.ts.map +1 -1
  7. package/dist/lib/format.js +11 -12
  8. package/dist/lib/format.js.map +1 -1
  9. package/dist/lib/lenses.d.ts +35 -73
  10. package/dist/lib/lenses.d.ts.map +1 -1
  11. package/dist/lib/lenses.js +144 -126
  12. package/dist/lib/lenses.js.map +1 -1
  13. package/dist/lib/repo.d.ts +11 -22
  14. package/dist/lib/repo.d.ts.map +1 -1
  15. package/dist/lib/repo.js +12 -25
  16. package/dist/lib/repo.js.map +1 -1
  17. package/dist/lib/scores.d.ts +15 -16
  18. package/dist/lib/scores.d.ts.map +1 -1
  19. package/dist/lib/scores.js +4 -11
  20. package/dist/lib/scores.js.map +1 -1
  21. package/dist/lib/search.d.ts +47 -129
  22. package/dist/lib/search.d.ts.map +1 -1
  23. package/dist/lib/search.js +195 -180
  24. package/dist/lib/search.js.map +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +7 -21
  26. package/dist/lib/taxonomy-counts.d.ts.map +1 -1
  27. package/dist/lib/taxonomy-counts.js +12 -31
  28. package/dist/lib/taxonomy-counts.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/components/AppsIndexRow.astro +55 -184
  31. package/src/components/AppsPagination.astro +45 -108
  32. package/src/components/CategoryGrid.astro +43 -31
  33. package/src/components/ContributorsGrid.astro +3 -3
  34. package/src/components/CurationGrid.astro +41 -0
  35. package/src/components/ExploreByStack.astro +6 -0
  36. package/src/components/FilterGroupMenu.astro +49 -0
  37. package/src/components/FilterOptions.astro +44 -0
  38. package/src/components/Hero.astro +198 -147
  39. package/src/components/Icon.astro +10 -3
  40. package/src/components/ItemCard.astro +266 -248
  41. package/src/components/RecordSection.astro +68 -33
  42. package/src/components/RefinePanel.astro +115 -141
  43. package/src/components/StackGrid.astro +32 -24
  44. package/src/components/WhyThisExists.astro +32 -32
  45. package/src/layouts/BaseLayout.astro +104 -49
  46. package/src/layouts/Container.astro +5 -44
  47. package/src/layouts/Footer.astro +31 -20
  48. package/src/layouts/Header.astro +40 -19
  49. package/src/layouts/SectionHeader.astro +27 -43
  50. package/src/layouts/Seo.astro +38 -56
  51. package/src/layouts/ThemeToggle.astro +35 -48
  52. package/src/lib/display.ts +46 -53
  53. package/src/lib/format.ts +11 -12
  54. package/src/lib/lenses.ts +151 -172
  55. package/src/lib/repo.ts +16 -28
  56. package/src/lib/scores.ts +15 -19
  57. package/src/lib/search.ts +222 -290
  58. package/src/lib/taxonomy-counts.ts +12 -46
  59. package/src/styles.css +207 -1
  60. package/templates/default/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  61. package/templates/default/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  62. package/templates/default/.github/ISSUE_TEMPLATE/record_submission.md +48 -0
  63. package/templates/default/.github/pull_request_template.md +27 -0
  64. package/templates/default/.github/workflows/build.yml +55 -0
  65. package/templates/default/.github/workflows/cleanup-stale-records.yml +48 -0
  66. package/templates/default/.github/workflows/daily-refresh.yml +53 -0
  67. package/templates/default/.github/workflows/sync-contributors.yml +53 -0
  68. package/templates/default/.github/workflows/sync-github-metadata.yml +58 -0
  69. package/templates/default/.github/workflows/validate-data.yml +35 -0
  70. package/templates/default/data/records/coolify.yml +46 -0
  71. package/templates/default/data/records/gitea.yml +46 -0
  72. package/templates/default/data/records/hoppscotch.yml +47 -0
  73. package/templates/default/data/records/inventree.yml +47 -0
  74. package/templates/default/data/records/label-studio.yml +46 -0
  75. package/templates/default/data/records/logseq.yml +48 -0
  76. package/templates/default/data/records/private-gpt.yml +47 -0
  77. package/templates/default/data/records/wakapi.yml +44 -0
  78. package/templates/default/data/taxonomy/categories.yml +47 -0
  79. package/templates/default/data/taxonomy/distribution-channels.yml +43 -0
  80. package/templates/default/data/taxonomy/platforms.yml +27 -0
  81. package/templates/default/data/taxonomy/stacks.yml +41 -0
  82. package/templates/default/grove.config.ts +1 -1
  83. package/templates/default/package.json +5 -4
  84. package/templates/default/public/icons/stacks/bun.svg +1 -0
  85. package/templates/default/public/icons/stacks/clojure.svg +1 -0
  86. package/templates/default/public/icons/stacks/clojurescript.svg +1 -0
  87. package/templates/default/public/icons/stacks/deno.svg +1 -0
  88. package/templates/default/public/icons/stacks/django.svg +1 -0
  89. package/templates/default/public/icons/stacks/docker.svg +1 -0
  90. package/templates/default/public/icons/stacks/go.svg +1 -0
  91. package/templates/default/public/icons/stacks/llm.svg +1 -0
  92. package/templates/default/public/icons/stacks/node.js.svg +1 -0
  93. package/templates/default/public/icons/stacks/sveltekit.svg +1 -0
  94. package/templates/default/public/icons/stacks/vue.svg +1 -0
  95. package/templates/default/public/llms-full.txt +197 -5
  96. package/templates/default/public/llms.txt +107 -6
  97. package/templates/default/public/sitemap.xml +63 -0
  98. package/templates/default/scripts/build-llms.mjs +123 -0
  99. package/templates/default/scripts/build-records-json.mjs +27 -0
  100. package/templates/default/scripts/build-sitemap.mjs +20 -0
  101. package/templates/default/scripts/cleanup-stale-records.mjs +20 -0
  102. package/templates/default/scripts/enrich-github-metadata.mjs +99 -0
  103. package/templates/default/scripts/fetch-icons.mjs +137 -0
  104. package/templates/default/scripts/migrate-legacy-to-schema-v1.mjs +86 -0
  105. package/templates/default/scripts/parse-legacy-readme.mjs +59 -0
  106. package/templates/default/scripts/refresh-records-activity.mjs +24 -0
  107. package/templates/default/scripts/repair-license-format.mjs +72 -0
  108. package/templates/default/scripts/report-cleanup-candidates.mjs +21 -0
  109. package/templates/default/scripts/seed-from-github.mjs +62 -0
  110. package/templates/default/scripts/sync-contributors.mjs +145 -0
  111. package/templates/default/scripts/sync-github-metadata.mjs +28 -0
  112. package/templates/default/scripts/validate-records.mjs +27 -0
  113. package/templates/default/src/data/records.ts +134 -51
  114. package/templates/default/src/pages/404.astro +64 -0
  115. package/templates/default/src/pages/{projects/[slug].astro → [slug]/[itemSlug].astro} +236 -66
  116. package/templates/default/src/pages/[slug]/index.astro +476 -0
  117. package/templates/default/src/pages/about.astro +165 -82
  118. package/templates/default/src/pages/apps/[itemSlug].astro +301 -0
  119. package/templates/default/src/pages/apps/index.astro +146 -0
  120. package/templates/default/src/pages/contributors.astro +184 -0
  121. package/templates/default/src/pages/index.astro +119 -162
  122. package/templates/default/src/pages/submit.astro +276 -114
  123. package/templates/default/wrangler.jsonc +28 -0
  124. package/LICENSE +0 -21
  125. package/templates/default/data/generated/apps.json +0 -1
  126. package/templates/default/data/generated/records.full.json +0 -8
  127. package/templates/default/data/generated/records.index.json +0 -7
  128. package/templates/default/data/generated/records.json +0 -8
  129. package/templates/default/data/generated/repo-stats.json +0 -1
  130. package/templates/default/data/generated/site-config.json +0 -31
  131. package/templates/default/src/components/layout/BaseLayout.astro +0 -47
  132. package/templates/default/src/components/layout/Container.astro +0 -18
  133. package/templates/default/src/components/layout/Footer.astro +0 -27
  134. package/templates/default/src/components/layout/Header.astro +0 -43
  135. package/templates/default/src/lib/markdown.ts +0 -171
  136. package/templates/default/src/pages/projects/index.astro +0 -193
@@ -1,43 +0,0 @@
1
- ---
2
- import siteConfig from "../../data/generated/site-config.json";
3
- import { indexSlug } from "../../data/records";
4
-
5
- const navItems = siteConfig.nav.length > 0
6
- ? siteConfig.nav
7
- : [
8
- { label: "Home", href: "/" },
9
- { label: "Browse", href: `/${indexSlug()}` },
10
- { label: "About", href: "/about" },
11
- { label: "Submit", href: "/submit" },
12
- ];
13
- ---
14
-
15
- <header class="border-b border-ink-200 bg-white/85 backdrop-blur dark:border-ink-800 dark:bg-ink-950/85">
16
- <div class="mx-auto flex max-w-6xl items-center gap-6 px-5 py-3 sm:px-7">
17
- <a
18
- href="/"
19
- class="inline-flex items-center gap-2 text-base font-semibold tracking-tight text-ink-900 dark:text-ink-100"
20
- >
21
- <span
22
- aria-hidden="true"
23
- class="inline-block h-4 w-4 rounded bg-ink-900 dark:bg-ink-100"
24
- ></span>
25
- <span>{siteConfig.name}</span>
26
- </a>
27
-
28
- <nav class="hidden flex-1 md:block" aria-label="Primary">
29
- <ul class="m-0 flex list-none gap-5 p-0">
30
- {navItems.map((item) => (
31
- <li>
32
- <a
33
- href={item.href}
34
- class="text-sm font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
35
- >
36
- {item.label}
37
- </a>
38
- </li>
39
- ))}
40
- </ul>
41
- </nav>
42
- </div>
43
- </header>
@@ -1,171 +0,0 @@
1
- /**
2
- * Tiny markdown → HTML converter for page-level content bodies.
3
- *
4
- * Scope is intentionally narrow: it handles the markdown the
5
- * `content/pages/*.md` files actually use (headings, paragraphs,
6
- * unordered + ordered lists, fenced code, inline `code` and
7
- * links). Anything more elaborate (tables, footnotes, autolinks)
8
- * is out of scope — consumers can swap in a real renderer
9
- * (marked, remark, ...) by overriding the page-level helper.
10
- *
11
- * The output is HTML-escaped at the source so a stray `<` in the
12
- * markdown file can't break the page. Inline `code` and links are
13
- * processed before the escape pass.
14
- */
15
- import { existsSync, readFileSync } from "node:fs";
16
-
17
- function escapeHtml(s: string): string {
18
- return s
19
- .replace(/&/g, "&amp;")
20
- .replace(/</g, "&lt;")
21
- .replace(/>/g, "&gt;");
22
- }
23
-
24
- function renderInline(line: string): string {
25
- let out = escapeHtml(line);
26
- // Inline code: `…` — render first so its content doesn't get
27
- // further processed.
28
- out = out.replace(/`([^`]+)`/g, (_, code: string) => `<code>${code}</code>`);
29
- // Links: [text](url)
30
- out = out.replace(
31
- /\[([^\]]+)\]\(([^)]+)\)/g,
32
- (_, text: string, url: string) =>
33
- `<a href="${url}" class="text-ink-900 underline hover:text-ink-700 dark:text-ink-100 dark:hover:text-white" rel="noopener noreferrer">${text}</a>`,
34
- );
35
- // Bold: **…**
36
- out = out.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
37
- // Italic: *…*
38
- out = out.replace(/\*([^*]+)\*/g, "<em>$1</em>");
39
- return out;
40
- }
41
-
42
- interface Block {
43
- type: "h2" | "h3" | "p" | "ul" | "ol" | "pre";
44
- lines: string[];
45
- }
46
-
47
- function parseBlocks(md: string): Block[] {
48
- const lines = md.split(/\r?\n/);
49
- const blocks: Block[] = [];
50
- let i = 0;
51
- while (i < lines.length) {
52
- const line = lines[i];
53
- if (!line.trim()) {
54
- i++;
55
- continue;
56
- }
57
- if (line.startsWith("## ")) {
58
- blocks.push({ type: "h2", lines: [line.slice(3)] });
59
- i++;
60
- continue;
61
- }
62
- if (line.startsWith("### ")) {
63
- blocks.push({ type: "h3", lines: [line.slice(4)] });
64
- i++;
65
- continue;
66
- }
67
- if (line.startsWith("```")) {
68
- const buf: string[] = [];
69
- i++;
70
- while (i < lines.length && !lines[i].startsWith("```")) {
71
- buf.push(lines[i]);
72
- i++;
73
- }
74
- i++; // skip closing fence
75
- blocks.push({ type: "pre", lines: buf });
76
- continue;
77
- }
78
- if (/^[-*] /.test(line)) {
79
- const buf: string[] = [];
80
- while (i < lines.length && /^[-*] /.test(lines[i])) {
81
- buf.push(lines[i].slice(2));
82
- i++;
83
- }
84
- blocks.push({ type: "ul", lines: buf });
85
- continue;
86
- }
87
- if (/^\d+\. /.test(line)) {
88
- const buf: string[] = [];
89
- while (i < lines.length && /^\d+\. /.test(lines[i])) {
90
- buf.push(lines[i].replace(/^\d+\. /, ""));
91
- i++;
92
- }
93
- blocks.push({ type: "ol", lines: buf });
94
- continue;
95
- }
96
- // Default: paragraph (consume contiguous non-blank lines).
97
- const buf: string[] = [line];
98
- i++;
99
- while (
100
- i < lines.length &&
101
- lines[i].trim() &&
102
- !lines[i].startsWith("## ") &&
103
- !lines[i].startsWith("### ") &&
104
- !lines[i].startsWith("```") &&
105
- !/^[-*] /.test(lines[i]) &&
106
- !/^\d+\. /.test(lines[i])
107
- ) {
108
- buf.push(lines[i]);
109
- i++;
110
- }
111
- blocks.push({ type: "p", lines: buf });
112
- }
113
- return blocks;
114
- }
115
-
116
- export function renderMarkdown(md: string): string {
117
- const blocks = parseBlocks(md);
118
- const html: string[] = [];
119
- for (const b of blocks) {
120
- switch (b.type) {
121
- case "h2":
122
- html.push(
123
- `<h2 class="m-0 mt-10 text-[1.375rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h2>`,
124
- );
125
- break;
126
- case "h3":
127
- html.push(
128
- `<h3 class="m-0 mt-8 text-[1.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">${renderInline(b.lines[0])}</h3>`,
129
- );
130
- break;
131
- case "p":
132
- html.push(
133
- `<p class="m-0 mt-4 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines.map(renderInline).join(" ")}</p>`,
134
- );
135
- break;
136
- case "ul":
137
- html.push(
138
- `<ul class="m-0 mt-4 list-disc space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
139
- .map((l) => `<li>${renderInline(l)}</li>`)
140
- .join("")}</ul>`,
141
- );
142
- break;
143
- case "ol":
144
- html.push(
145
- `<ol class="m-0 mt-4 list-decimal space-y-1.5 pl-6 text-[0.9375rem] leading-relaxed text-ink-700 dark:text-ink-200">${b.lines
146
- .map((l) => `<li>${renderInline(l)}</li>`)
147
- .join("")}</ol>`,
148
- );
149
- break;
150
- case "pre":
151
- html.push(
152
- `<pre class="mt-4 overflow-x-auto rounded-md border border-ink-200 bg-ink-50 p-4 text-xs leading-relaxed text-ink-800 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-200"><code>${escapeHtml(b.lines.join("\n"))}</code></pre>`,
153
- );
154
- break;
155
- }
156
- }
157
- return html.join("\n");
158
- }
159
-
160
- /** Read a markdown file from a list of candidate paths, return null if missing. */
161
- export function readMarkdownFile(candidates: string[]): string | null {
162
- for (const path of candidates) {
163
- try {
164
- if (!existsSync(path)) continue;
165
- return readFileSync(path, "utf8");
166
- } catch {
167
- continue;
168
- }
169
- }
170
- return null;
171
- }
@@ -1,193 +0,0 @@
1
- ---
2
- /**
3
- * List page — the discovery / browse page.
4
- *
5
- * Renders the full filter / search / sort machinery:
6
- * - SmartLensTabs (All / Trending / Recently added / Established / Featured / Needs review)
7
- * - RefinePanel (Stack / Platform / Category / License / Sort multi-selects)
8
- * - AppsIndexRow list (compact table-row per record)
9
- * - AppsPagination (windowed page list)
10
- * - ExploreByCategory / ExploreByStack (compact pill lists)
11
- *
12
- * Pure server-rendered: every interaction is a GET to the same URL
13
- * with a different query string. No client-side filter script — the
14
- * page rebuilds on every request because Astro's default output is
15
- * static (all pages are baked at build time and revalidated by the
16
- * deploy target).
17
- */
18
- import siteConfig from "../../../data/generated/site-config.json";
19
- import { records, indexSlug, itemLabel } from "../../data/records";
20
- import BaseLayout from "@grove-dev/astro/layouts/BaseLayout.astro";
21
- import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
22
- import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
23
- import AppsIndexRow from "@grove-dev/astro/components/AppsIndexRow.astro";
24
- import AppsPagination from "@grove-dev/astro/components/AppsPagination.astro";
25
- import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
26
- import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
27
- import {
28
- filtersFromSearchParams,
29
- filterApps,
30
- applySort,
31
- paginate,
32
- totalPages,
33
- effectivePage,
34
- buildFacets,
35
- PAGE_SIZE,
36
- pipeline,
37
- } from "@grove-dev/astro";
38
-
39
- const slug = indexSlug();
40
- const total = records.length;
41
-
42
- const title = `Browse ${slug} — ${siteConfig.name}`;
43
- const description = `A searchable directory of ${slug} for ${siteConfig.name}.`;
44
-
45
- // ── URL → filters ───────────────────────────────────────────────
46
- const filters = filtersFromSearchParams(Astro.url.searchParams);
47
-
48
- // ── Facet map (built from the FULL record set, not the filtered
49
- // one, so users can see all available options even when none of
50
- // them are active) ───────────────────────────────────────────────
51
- const facets = buildFacets(records);
52
-
53
- // ── Lens → filter → sort pipeline (matches the openapps list) ──
54
- const filtered = pipeline(records, filters);
55
-
56
- // ── Pagination ───────────────────────────────────────────────────
57
- const page = effectivePage(filters);
58
- const pages = totalPages(filtered.length, PAGE_SIZE);
59
- const pageItems = paginate(filtered, page, PAGE_SIZE);
60
-
61
- // ── "Explore" facet pills (top 12 categories / stacks by count) ─
62
- const categoryEntries = facets.categories.slice(0, 12).map((f) => ({
63
- name: f.value,
64
- slug: f.value,
65
- count: f.count,
66
- }));
67
- const stackEntries = facets.stacks.slice(0, 18).map((f) => ({
68
- name: f.value,
69
- slug: f.value,
70
- count: f.count,
71
- }));
72
-
73
- function hrefForPage(p: number): string {
74
- const sp = new URLSearchParams(Astro.url.searchParams);
75
- if (p <= 1) sp.delete("page");
76
- else sp.set("page", String(p));
77
- const qs = sp.toString();
78
- return qs ? `/${slug}?${qs}` : `/${slug}`;
79
- }
80
- ---
81
-
82
- <BaseLayout
83
- title={title}
84
- description={description}
85
- site={siteConfig}
86
- submitHref="/submit"
87
- submitLabel={`Submit ${itemLabel()}`}
88
- >
89
- <section class="grove-section-tight border-b border-ink-200 dark:border-ink-800">
90
- <div class="grove-container-wide mx-auto px-5 pt-10 sm:px-7 sm:pt-12">
91
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
92
- {slug}
93
- </span>
94
- <h1 class="m-0 mt-1 text-[1.875rem] sm:text-[2.125rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
95
- Browse {slug}
96
- </h1>
97
- <p class="m-0 mt-2 max-w-[64ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
98
- Search, filter, and explore {total} {itemLabel()}{total === 1 ? "" : "s"} in this directory.
99
- </p>
100
-
101
- <div class="mt-6">
102
- <SmartLensTabs
103
- currentParams={Astro.url.searchParams}
104
- pathPrefix={`/${slug}`}
105
- />
106
- </div>
107
-
108
- <div class="mt-5">
109
- <RefinePanel
110
- facets={facets}
111
- initial={filters}
112
- pathPrefix={`/${slug}`}
113
- />
114
- </div>
115
- </div>
116
- </section>
117
-
118
- {categoryEntries.length > 0 && (
119
- <ExploreByCategory
120
- categories={categoryEntries}
121
- pathPrefix={`/${slug}`}
122
- />
123
- )}
124
-
125
- {stackEntries.length > 0 && (
126
- <ExploreByStack
127
- stacks={stackEntries}
128
- pathPrefix={`/${slug}`}
129
- />
130
- )}
131
-
132
- <section class="grove-section-tight">
133
- <div class="grove-container-wide mx-auto px-5 sm:px-7">
134
- <div class="mb-4 flex flex-wrap items-baseline justify-between gap-2">
135
- <p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
136
- Showing
137
- <span class="ml-1 font-mono text-ink-900 dark:text-ink-100">
138
- {filtered.length === 0 ? 0 : (page - 1) * PAGE_SIZE + 1}
139
- </span>
140
- –
141
- <span class="font-mono text-ink-900 dark:text-ink-100">
142
- {Math.min(page * PAGE_SIZE, filtered.length)}
143
- </span>
144
- of
145
- <span class="ml-1 font-mono text-ink-900 dark:text-ink-100">
146
- {filtered.length}
147
- </span>
148
- {itemLabel()}{filtered.length === 1 ? "" : "s"}
149
- </p>
150
- {pages > 1 && (
151
- <AppsPagination
152
- current={page}
153
- total={pages}
154
- hrefFor={hrefForPage}
155
- />
156
- )}
157
- </div>
158
-
159
- {pageItems.length === 0 ? (
160
- <div class="flex flex-col items-center gap-2 py-16 text-center">
161
- <p class="m-0 text-[0.9375rem] font-medium text-ink-700 dark:text-ink-200">
162
- No {itemLabel()}s match these filters.
163
- </p>
164
- <a
165
- href={`/${slug}`}
166
- class="mt-3 text-sm text-ink-500 hover:text-ink-900 dark:hover:text-ink-100"
167
- >
168
- Clear all filters
169
- </a>
170
- </div>
171
- ) : (
172
- <div class="flex flex-col gap-3">
173
- {pageItems.map((record) => (
174
- <AppsIndexRow
175
- record={record}
176
- detailHref={`/${slug}/${record.slug}`}
177
- />
178
- ))}
179
- </div>
180
- )}
181
-
182
- {pages > 1 && (
183
- <div class="mt-6 flex justify-center">
184
- <AppsPagination
185
- current={page}
186
- total={pages}
187
- hrefFor={hrefForPage}
188
- />
189
- </div>
190
- )}
191
- </div>
192
- </section>
193
- </BaseLayout>