Skip to content

Commit e6cff14

Browse files
committed
Add directory for all libraries
1 parent e9266fb commit e6cff14

File tree

5 files changed

+159
-11
lines changed

5 files changed

+159
-11
lines changed

src/components/Button/index.astro

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
interface Props {
3+
href: string
4+
selected?: boolean
5+
}
6+
const { href, selected = false } = Astro.props;
7+
---
8+
9+
<a href={href} class={`rounded-button ${selected ? 'selected' : ''}`}>
10+
<slot />
11+
</a>
12+
13+
<style lang="scss">
14+
.rounded-button {
15+
display: inline-block;
16+
margin: var(--spacing-md);
17+
margin-left: 0;
18+
padding: var(--spacing-xs) var(--spacing-sm);
19+
border: 1px solid var(--type-black);
20+
color: var(--type-black);
21+
border-radius: 999px;
22+
}
23+
.rounded-button.selected {
24+
color: var(--type-black);
25+
background: var(--accent-color);
26+
}
27+
</style>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
import { getCurrentLocale } from "@/src/i18n/utils";
3+
import { getLibraryLink } from "@/src/pages/_utils";
4+
import Image from "@components/Image/index.astro";
5+
import type { CollectionEntry } from "astro:content";
6+
7+
interface Props {
8+
item: CollectionEntry<"libraries">;
9+
}
10+
11+
const { item } = Astro.props;
12+
13+
const authorsFormatter = new Intl.ListFormat(
14+
getCurrentLocale(Astro.url.pathname),
15+
{
16+
style: "long",
17+
type: "conjunction",
18+
}
19+
);
20+
const authorsString = authorsFormatter.format(
21+
item.data.author.map((a) => a.name)
22+
);
23+
let description = item.data.description.trim();
24+
// If the description didn't end with punctuation, add it, since we will be
25+
// appending another sentence afterwards.
26+
if (!/[.!]\)?$/.test(description)) {
27+
description += ".";
28+
}
29+
---
30+
31+
<a class="group hover:no-underline flex mt-sm items-center" href={getLibraryLink(item)}>
32+
<Image
33+
src={item.data.featuredImage}
34+
alt={item.data.featuredImageAlt}
35+
width="80"
36+
class="mr-2"
37+
/>
38+
<div class="flex-1">
39+
<!-- visible alt text class keeps the alt text within
40+
the narrower image width given in class prop -->
41+
<p
42+
class="text-xl mt-0 text-wrap break-words break-keep group-hover:underline"
43+
>
44+
{item.data.name}
45+
</p>
46+
<p class="text-body-caption mt-xxs">
47+
{description} By {authorsString}
48+
</p>
49+
</div>
50+
</a>

src/content/ui/en.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,7 @@ calloutTitles:
184184
"Try this!": "Try this!"
185185
Tip: Tip
186186
Note: Note
187+
LibrariesLayout:
188+
View All: View All
189+
Featured: Featured
190+
Everything: Everything

src/layouts/LibrariesLayout.astro

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ import type { CollectionEntry } from "astro:content";
33
import Head from "@components/Head/index.astro";
44
import BaseLayout from "./BaseLayout.astro";
55
import GridItemLibrary from "@components/GridItem/Library.astro";
6+
import LibraryListing from "@components/LibraryListing/index.astro";
67
import { setJumpToState, type JumpToLink } from "../globals/state";
78
import { getCurrentLocale, getUiTranslator } from "../i18n/utils";
89
import { categories } from "../content/libraries/config";
10+
import Button from "@components/Button/index.astro";
11+
import _ from "lodash";
912
1013
interface Props {
1114
entries: CollectionEntry<"libraries">[];
1215
page: CollectionEntry<"pages">;
1316
title: string;
17+
full?: boolean;
1418
}
1519
type LibraryEntry = CollectionEntry<"libraries">;
1620
1721
const currentLocale = getCurrentLocale(Astro.url.pathname);
1822
const t = await getUiTranslator(currentLocale);
1923
20-
const { entries, page } = Astro.props;
24+
const { entries, page, full } = Astro.props;
2125
const { Content } = await page.render();
2226
2327
function strCompare(a: string, b: string) {
@@ -30,19 +34,56 @@ function strCompare(a: string, b: string) {
3034
return 0;
3135
}
3236
33-
const sections = categories
37+
let sections = categories
3438
.map((slug) => {
3539
const name = t("libraryCategories", slug);
36-
const sectionEntries = entries
40+
let sectionEntries = entries
3741
.filter((e: LibraryEntry) => e.data.category === slug)
3842
.sort((a: LibraryEntry, b: LibraryEntry) =>
3943
strCompare(a.data.name.toLowerCase(), b.data.name.toLowerCase())
4044
);
4145
46+
4247
return { slug, name, sectionEntries };
4348
})
4449
.filter((section) => section.sectionEntries.length > 0);
4550
51+
if (!full) {
52+
// On the featured libraries page, we want to show as close to 4 entries
53+
// per section as possible, while also trying to give all contributors
54+
// approximately equal footing of being featured. To do this, we don't
55+
// let contributors show up >3x on the featured page, and we try a
56+
// Monte Carlo approach to try to get as close to this as possible.
57+
const targetEntriesPerSection = 4
58+
59+
let minScore = 1000
60+
let bestSections = sections
61+
for (let attempt = 0; attempt < 100; attempt++) {
62+
const entriesByAuthor = _.groupBy(entries, (e: LibraryEntry) => e.data.author[0].name)
63+
const toRemove = new Set()
64+
for (const key in entriesByAuthor) {
65+
if (entriesByAuthor[key].length > 3) {
66+
for (const entry of _.shuffle(entriesByAuthor[key]).slice(3)) {
67+
toRemove.add(entry.id)
68+
}
69+
}
70+
}
71+
const candidateSections = sections.map((s) => ({
72+
...s,
73+
sectionEntries: s.sectionEntries.filter((e) => !toRemove.has(e.id)).slice(0, targetEntriesPerSection),
74+
allEntries: s.sectionEntries,
75+
}));
76+
const score = candidateSections
77+
.map((s) => Math.abs(s.sectionEntries.length - targetEntriesPerSection))
78+
.reduce((acc, next) => acc + next, 0);
79+
if (score < minScore) {
80+
minScore = score;
81+
bestSections = candidateSections;
82+
}
83+
}
84+
sections = bestSections;
85+
}
86+
4687
const pageJumpToLinks = categories.map((category) => ({
4788
url: `/libraries#${category}`,
4889
label: t("libraryCategories", category),
@@ -66,17 +107,33 @@ setJumpToState({
66107
<Content />
67108
</div>
68109

110+
<div class="flex">
111+
<Button selected={!full} href="/libraries">{t("LibrariesLayout", "Featured")}</Button>
112+
<Button selected={full} href="/libraries/directory">{t("LibrariesLayout", "Everything")}</Button>
113+
</div>
114+
69115
{
70-
sections.map(({ slug, name, sectionEntries }) => (
116+
sections.map(({ slug, name, sectionEntries, allEntries }) => (
71117
<section>
72118
<h2 id={slug}>{name}</h2>
73-
<ul class="content-grid-simple">
74-
{sectionEntries.map((entry: LibraryEntry) => (
75-
<li>
76-
<GridItemLibrary item={entry} />
77-
</li>
78-
))}
79-
</ul>
119+
{full ? (
120+
<>
121+
{sectionEntries.map((entry: LibraryEntry) => (
122+
<LibraryListing item={entry} />
123+
))}
124+
</>
125+
) : (
126+
<>
127+
<ul class="content-grid-simple">
128+
{sectionEntries.map((entry: LibraryEntry) => (
129+
<li>
130+
<GridItemLibrary item={entry} />
131+
</li>
132+
))}
133+
</ul>
134+
<Button href={`/libraries/directory/#${slug}`}>{t("LibrariesLayout", "View All")} ({allEntries.length})</Button>
135+
</>
136+
)}
80137
</section>
81138
))
82139
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
import { getCollectionInDefaultLocale } from "../../_utils";
3+
import LibrariesLayout from "@/src/layouts/LibrariesLayout.astro";
4+
5+
const libraries = await getCollectionInDefaultLocale("libraries");
6+
const pages = await getCollectionInDefaultLocale("pages");
7+
const page = pages.find((page) => page.slug === 'libraries')!
8+
---
9+
10+
<LibrariesLayout full title="Libraries" entries={libraries} page={page} />

0 commit comments

Comments
 (0)