lightnet 3.12.1 → 4.0.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/CHANGELOG.md +55 -0
- package/__e2e__/basics-fixture.ts +9 -63
- package/__e2e__/fixtures/basics/astro.config.mjs +7 -12
- package/__e2e__/fixtures/basics/node_modules/.bin/astro +4 -4
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwind +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tailwindcss +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsc +2 -2
- package/__e2e__/fixtures/basics/node_modules/.bin/tsserver +2 -2
- package/__e2e__/fixtures/basics/package.json +6 -5
- package/__e2e__/fixtures/basics/src/content/categories/christian-living.json +4 -1
- package/__e2e__/fixtures/basics/src/content/categories/teens.json +4 -1
- package/__e2e__/fixtures/basics/src/content/categories/theology.json +4 -1
- package/__e2e__/fixtures/basics/src/content/media/faithful-freestyle--en.json +6 -2
- package/__e2e__/fixtures/basics/src/content/media/how-to-kickflip--de.json +6 -1
- package/__e2e__/fixtures/basics/src/content/media/skate-sounds--en.json +8 -2
- package/__e2e__/fixtures/basics/src/content/media-collections/how-to-articles.json +5 -1
- package/__e2e__/fixtures/basics/src/content/media-types/audio.json +5 -2
- package/__e2e__/fixtures/basics/src/content/media-types/book.json +10 -4
- package/__e2e__/fixtures/basics/src/content/media-types/video.json +5 -2
- package/__e2e__/fixtures/basics/src/pages/[locale]/index.astro +0 -1
- package/__e2e__/fixtures/basics/src/translations/de.yml +0 -8
- package/__e2e__/fixtures/basics/src/translations/en.yml +0 -8
- package/__e2e__/global.teardown.ts +2 -2
- package/__tests__/astro-integration/config.spec.ts +364 -0
- package/__tests__/astro-integration/integration.spec.ts +125 -0
- package/__tests__/astro-integration/tailwind.spec.ts +36 -0
- package/__tests__/content/content-schema.spec.ts +109 -0
- package/__tests__/content/get-media-collections.spec.ts +72 -0
- package/__tests__/content/query-media-items.spec.ts +213 -0
- package/__tests__/i18n/resolve-current-locale.spec.ts +65 -0
- package/__tests__/i18n/translate-map.spec.ts +19 -0
- package/__tests__/i18n/translate.spec.ts +91 -0
- package/__tests__/pages/details-page/create-content-metadata.spec.ts +43 -25
- package/__tests__/pages/details-page/get-translations.spec.ts +56 -0
- package/__tests__/utils/paths.spec.ts +116 -0
- package/__tests__/utils/urls.spec.ts +9 -4
- package/exports/content.ts +7 -2
- package/exports/i18n.ts +0 -1
- package/exports/index.ts +1 -5
- package/exports/utils.ts +0 -1
- package/package.json +16 -12
- package/src/astro-integration/config.ts +60 -49
- package/src/astro-integration/integration.ts +13 -24
- package/src/astro-integration/tailwind.ts +86 -0
- package/src/astro-integration/validators/validate-inline-translations.ts +51 -0
- package/src/astro-integration/validators/validate-languages.ts +39 -0
- package/src/astro-integration/virtual.d.ts +8 -6
- package/src/astro-integration/vite-plugin-lightnet-config.ts +29 -9
- package/src/components/CarouselSection.astro +7 -11
- package/src/components/CategoriesSection.astro +2 -2
- package/src/components/HighlightSection.astro +4 -7
- package/src/components/Icon.tsx +2 -2
- package/src/components/MediaGallerySection.astro +88 -68
- package/src/components/MediaList.astro +9 -7
- package/src/components/SearchInput.astro +7 -4
- package/src/components/Section.astro +7 -5
- package/src/components/VideoPlayer.astro +2 -3
- package/src/content/content-schema.ts +129 -149
- package/src/content/get-categories.ts +52 -28
- package/src/content/get-languages.ts +29 -8
- package/src/content/get-media-collections.ts +43 -0
- package/src/content/get-media-types.ts +41 -7
- package/src/content/query-media-items.ts +23 -13
- package/src/i18n/bcp-47.ts +8 -0
- package/src/i18n/get-locale-paths.ts +1 -3
- package/src/i18n/locals.d.ts +21 -3
- package/src/i18n/locals.ts +18 -11
- package/src/i18n/resolve-current-locale.ts +18 -0
- package/src/i18n/resolve-language.ts +10 -5
- package/src/i18n/translate-map.ts +70 -0
- package/src/i18n/translate.ts +68 -47
- package/src/layouts/Page.astro +5 -3
- package/src/layouts/components/LanguagePicker.astro +22 -17
- package/src/layouts/components/Menu.astro +2 -5
- package/src/layouts/components/MenuItem.astro +1 -1
- package/src/layouts/components/PageNavigation.astro +29 -29
- package/src/layouts/components/PageTitle.astro +23 -7
- package/src/pages/404Route.astro +2 -1
- package/src/pages/RootRoute.astro +6 -1
- package/src/pages/details-page/DefaultDetailsPage.astro +9 -2
- package/src/pages/details-page/DetailsPageRoute.astro +1 -2
- package/src/pages/details-page/components/AudioPanel.astro +7 -3
- package/src/pages/details-page/components/AudioPlayer.astro +2 -2
- package/src/pages/details-page/components/ContentSection.astro +67 -44
- package/src/pages/details-page/components/MediaCollection.astro +8 -4
- package/src/pages/details-page/components/MediaCollectionsSection.astro +3 -6
- package/src/pages/details-page/components/main-details/EditButton.astro +22 -10
- package/src/pages/details-page/components/main-details/OpenButton.astro +17 -12
- package/src/pages/details-page/components/main-details/ShareButton.astro +3 -2
- package/src/pages/details-page/components/more-details/Categories.astro +5 -3
- package/src/pages/details-page/components/more-details/Languages.astro +12 -7
- package/src/pages/details-page/utils/create-content-metadata.ts +24 -9
- package/src/pages/details-page/utils/get-translations.ts +6 -0
- package/src/pages/search-page/components/LoadingSkeleton.tsx +6 -5
- package/src/pages/search-page/components/SearchFilter.astro +10 -21
- package/src/pages/search-page/components/SearchFilter.tsx +2 -2
- package/src/pages/search-page/components/SearchList.astro +10 -7
- package/src/pages/search-page/components/SearchListItem.tsx +5 -4
- package/src/pages/search-page/hooks/use-search.ts +5 -2
- package/src/utils/lazy.ts +20 -0
- package/src/utils/paths.ts +40 -3
- package/src/utils/urls.ts +1 -2
- package/src/utils/verify-schema.ts +12 -10
- package/tailwind.config.ts +1 -25
- package/vitest.config.js +18 -2
- package/src/astro-integration/project-context.ts +0 -5
- package/src/content/compare-media-collection-items.ts +0 -24
- package/src/i18n/resolve-default-locale.ts +0 -19
- package/src/i18n/resolve-locales.ts +0 -5
- package/src/pages/details-page/utils/get-collection-items.ts +0 -29
|
@@ -5,6 +5,22 @@ import { defineCollection, reference } from "astro:content"
|
|
|
5
5
|
|
|
6
6
|
import { imageSchema } from "./astro-image"
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Translations by BCP-47 tag
|
|
10
|
+
* Must contain at least one localized value.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* {
|
|
14
|
+
* de: "Hallo",
|
|
15
|
+
* en: "Hello"
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export const inlineTranslationSchema = z
|
|
19
|
+
.record(z.string(), z.string())
|
|
20
|
+
.refine((value) => Object.keys(value).length > 0, {
|
|
21
|
+
message: "Inline translations must contain at least one entry",
|
|
22
|
+
})
|
|
23
|
+
|
|
8
24
|
/**
|
|
9
25
|
* Category Schema
|
|
10
26
|
*/
|
|
@@ -12,12 +28,9 @@ export const categorySchema = z.object({
|
|
|
12
28
|
/**
|
|
13
29
|
* Name of the category.
|
|
14
30
|
*
|
|
15
|
-
*
|
|
16
|
-
* LightNet will try to use it as translation key first if no translation is found it will use the string as is.
|
|
17
|
-
*
|
|
18
|
-
* @example "category.biography"
|
|
31
|
+
* Label translated for the default locale. Other configured site locales are optional.
|
|
19
32
|
*/
|
|
20
|
-
label:
|
|
33
|
+
label: inlineTranslationSchema,
|
|
21
34
|
|
|
22
35
|
/* Relative path to the thumbnail image of this category.
|
|
23
36
|
*
|
|
@@ -37,10 +50,16 @@ export const mediaCollectionSchema = z.object({
|
|
|
37
50
|
/**
|
|
38
51
|
* Name of the collection.
|
|
39
52
|
*
|
|
40
|
-
*
|
|
41
|
-
|
|
53
|
+
* Label translated for the default locale. Other configured site locales are optional.
|
|
54
|
+
*/
|
|
55
|
+
label: inlineTranslationSchema,
|
|
56
|
+
/**
|
|
57
|
+
* Ordered list of media items included in this collection.
|
|
58
|
+
* The array order defines how items are shown when querying by collection.
|
|
59
|
+
*
|
|
60
|
+
* @example ["my-book--en", "my-video--en"]
|
|
42
61
|
*/
|
|
43
|
-
|
|
62
|
+
mediaItems: z.array(reference("media")),
|
|
44
63
|
})
|
|
45
64
|
|
|
46
65
|
/**
|
|
@@ -48,15 +67,16 @@ export const mediaCollectionSchema = z.object({
|
|
|
48
67
|
*/
|
|
49
68
|
export const mediaItemSchema = z.object({
|
|
50
69
|
/**
|
|
51
|
-
*
|
|
52
|
-
* share the same commonId they will show up as translations.
|
|
70
|
+
* Optional identifier used to link translated variants of a media item.
|
|
71
|
+
* If other media items share the same commonId they will show up as translations.
|
|
72
|
+
* If omitted, the media item is treated as standalone and has no translations.
|
|
53
73
|
* The common id will show up in the media item's url combined with it's language.
|
|
54
74
|
*
|
|
55
75
|
* We suggest you use the english name of the media item, all lower case, words separated with hyphens.
|
|
56
76
|
*
|
|
57
77
|
* @example "a-book-about-love"
|
|
58
78
|
*/
|
|
59
|
-
commonId: z.string(),
|
|
79
|
+
commonId: z.string().optional(),
|
|
60
80
|
/**
|
|
61
81
|
* Title of this media item.
|
|
62
82
|
* This is expected to be in the language that is defined by the 'language' property.
|
|
@@ -89,7 +109,7 @@ export const mediaItemSchema = z.object({
|
|
|
89
109
|
*
|
|
90
110
|
* @example 2024-09-10
|
|
91
111
|
*/
|
|
92
|
-
dateCreated: z.
|
|
112
|
+
dateCreated: z.iso.date(),
|
|
93
113
|
/**
|
|
94
114
|
* List of categories of this media item.
|
|
95
115
|
*
|
|
@@ -97,31 +117,11 @@ export const mediaItemSchema = z.object({
|
|
|
97
117
|
*/
|
|
98
118
|
categories: z.array(reference("categories")).nullish(),
|
|
99
119
|
/**
|
|
100
|
-
*
|
|
101
|
-
* Collections can be used to group media items into series, playlists...
|
|
102
|
-
*
|
|
103
|
-
* @example [{collection:"my-series"}]
|
|
104
|
-
*/
|
|
105
|
-
collections: z
|
|
106
|
-
.array(
|
|
107
|
-
z.object({
|
|
108
|
-
/**
|
|
109
|
-
* Id of the collection.
|
|
110
|
-
*/
|
|
111
|
-
collection: reference("media-collections"),
|
|
112
|
-
/**
|
|
113
|
-
* Position of the item inside the collection.
|
|
114
|
-
*/
|
|
115
|
-
index: z.number().optional(),
|
|
116
|
-
}),
|
|
117
|
-
)
|
|
118
|
-
.nullish(),
|
|
119
|
-
/**
|
|
120
|
-
* BCP-47 name of the language this media item is in.
|
|
120
|
+
* BCP-47 language code of this media item.
|
|
121
121
|
*
|
|
122
122
|
* @example "en"
|
|
123
123
|
*/
|
|
124
|
-
language: z.string(),
|
|
124
|
+
language: z.string().nonempty(),
|
|
125
125
|
/**
|
|
126
126
|
* Relative path to the image of this media item. Eg. a book cover or video thumbnail.
|
|
127
127
|
*
|
|
@@ -139,6 +139,15 @@ export const mediaItemSchema = z.object({
|
|
|
139
139
|
content: z
|
|
140
140
|
.array(
|
|
141
141
|
z.object({
|
|
142
|
+
/**
|
|
143
|
+
* Storage kind for this content item.
|
|
144
|
+
*
|
|
145
|
+
* - `"upload"`: a file managed by this LightNet site (typically a relative path like `/files/...`)
|
|
146
|
+
* - `"link"`: an external URL (typically `https://...`)
|
|
147
|
+
*
|
|
148
|
+
* @example "upload"
|
|
149
|
+
*/
|
|
150
|
+
type: z.enum(["upload", "link"]),
|
|
142
151
|
/**
|
|
143
152
|
* Urls might be:
|
|
144
153
|
* - links to youtube videos
|
|
@@ -153,10 +162,11 @@ export const mediaItemSchema = z.object({
|
|
|
153
162
|
*/
|
|
154
163
|
url: z.string(),
|
|
155
164
|
/**
|
|
156
|
-
* The name of the content
|
|
157
|
-
*
|
|
165
|
+
* The name of the content translated for the default locale.
|
|
166
|
+
* Other configured site locales are optional.
|
|
167
|
+
* If this is not set, the file name from URL will be used.
|
|
158
168
|
*/
|
|
159
|
-
label:
|
|
169
|
+
label: inlineTranslationSchema.optional(),
|
|
160
170
|
}),
|
|
161
171
|
)
|
|
162
172
|
.min(1),
|
|
@@ -183,111 +193,81 @@ export const createCategorySchema = ({ image }: SchemaContext) =>
|
|
|
183
193
|
/**
|
|
184
194
|
* Media Type Schema
|
|
185
195
|
*/
|
|
186
|
-
export const mediaTypeSchema = z
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
z.object({
|
|
262
|
-
/**
|
|
263
|
-
* Detail page for audio files.
|
|
264
|
-
*
|
|
265
|
-
* This only supports mp3 files.
|
|
266
|
-
*/
|
|
267
|
-
layout: z.literal("audio"),
|
|
268
|
-
}),
|
|
269
|
-
])
|
|
270
|
-
.optional(),
|
|
271
|
-
/**
|
|
272
|
-
* Pick the media type's icon from https://pictogrammers.com/library/mdi/
|
|
273
|
-
* Prefix it's name with "mdi--"
|
|
274
|
-
*
|
|
275
|
-
* @example "mdi--ab-testing"
|
|
276
|
-
*/
|
|
277
|
-
icon: z.string(),
|
|
278
|
-
})
|
|
279
|
-
.transform((mediaType) => {
|
|
280
|
-
// migrate old cover images style to new property
|
|
281
|
-
const hasDeprecatedBookCover =
|
|
282
|
-
mediaType.detailsPage?.layout === "default" &&
|
|
283
|
-
mediaType.detailsPage.coverStyle === "book"
|
|
284
|
-
return {
|
|
285
|
-
...mediaType,
|
|
286
|
-
coverImageStyle: hasDeprecatedBookCover
|
|
287
|
-
? "book"
|
|
288
|
-
: mediaType.coverImageStyle,
|
|
289
|
-
}
|
|
290
|
-
})
|
|
196
|
+
export const mediaTypeSchema = z.object({
|
|
197
|
+
/**
|
|
198
|
+
* Name of this media type that will be shown on the pages.
|
|
199
|
+
*
|
|
200
|
+
* Label translated for the default locale. Other configured site locales are optional.
|
|
201
|
+
*/
|
|
202
|
+
label: inlineTranslationSchema,
|
|
203
|
+
/**
|
|
204
|
+
* Defines how the cover image for a media item of this type is rendered.
|
|
205
|
+
*
|
|
206
|
+
* Options:
|
|
207
|
+
* - `"default"` — Renders the media item image with no modifications.
|
|
208
|
+
* - `"book"` — Adds a book fold effect and sharper edges, styled like a book cover.
|
|
209
|
+
* - `"video"` — Constrains the image to a 16:9 aspect ratio with a black background.
|
|
210
|
+
*
|
|
211
|
+
* @default "default"
|
|
212
|
+
*/
|
|
213
|
+
coverImageStyle: z.enum(["default", "book", "video"]).default("default"),
|
|
214
|
+
/**
|
|
215
|
+
* What media item details page to use for media items with this type.
|
|
216
|
+
*
|
|
217
|
+
*/
|
|
218
|
+
detailsPage: z
|
|
219
|
+
.discriminatedUnion("layout", [
|
|
220
|
+
z.object({
|
|
221
|
+
/**
|
|
222
|
+
* Details page for all media types.
|
|
223
|
+
*/
|
|
224
|
+
layout: z.literal("default"),
|
|
225
|
+
/**
|
|
226
|
+
* Label for the open action button. Use this if you want to change the text
|
|
227
|
+
* of the "Open" button to be more matching to your media item.
|
|
228
|
+
* For example you could change the text to be "Read" for a book media type.
|
|
229
|
+
*
|
|
230
|
+
* Label translated for the default locale. Other configured site locales are optional.
|
|
231
|
+
*/
|
|
232
|
+
openActionLabel: inlineTranslationSchema.optional(),
|
|
233
|
+
}),
|
|
234
|
+
z.object({
|
|
235
|
+
/**
|
|
236
|
+
* Custom details page.
|
|
237
|
+
*/
|
|
238
|
+
layout: z.literal("custom"),
|
|
239
|
+
/**
|
|
240
|
+
* This references a custom component name to be used for the
|
|
241
|
+
* details page. The custom component has be located at src/details-pages/
|
|
242
|
+
*
|
|
243
|
+
* @example "MyArticleDetails.astro"
|
|
244
|
+
*/
|
|
245
|
+
customComponent: z.string(),
|
|
246
|
+
}),
|
|
247
|
+
z.object({
|
|
248
|
+
/**
|
|
249
|
+
* Detail page for videos.
|
|
250
|
+
*/
|
|
251
|
+
layout: z.literal("video"),
|
|
252
|
+
}),
|
|
253
|
+
z.object({
|
|
254
|
+
/**
|
|
255
|
+
* Detail page for audio files.
|
|
256
|
+
*
|
|
257
|
+
* This only supports mp3 files.
|
|
258
|
+
*/
|
|
259
|
+
layout: z.literal("audio"),
|
|
260
|
+
}),
|
|
261
|
+
])
|
|
262
|
+
.optional(),
|
|
263
|
+
/**
|
|
264
|
+
* Pick the media type's icon from https://lucide.dev/icons/
|
|
265
|
+
* Prefix it's name with "lucide--"
|
|
266
|
+
*
|
|
267
|
+
* @example "lucide--book-open"
|
|
268
|
+
*/
|
|
269
|
+
icon: z.string(),
|
|
270
|
+
})
|
|
291
271
|
|
|
292
272
|
export const LIGHTNET_COLLECTIONS = {
|
|
293
273
|
categories: defineCollection({
|
|
@@ -315,13 +295,6 @@ export const LIGHTNET_COLLECTIONS = {
|
|
|
315
295
|
}),
|
|
316
296
|
schema: mediaTypeSchema,
|
|
317
297
|
}),
|
|
318
|
-
"internal-media-image-path": defineCollection({
|
|
319
|
-
loader: glob({
|
|
320
|
-
pattern: "*.json",
|
|
321
|
-
base: "./src/content/media",
|
|
322
|
-
}),
|
|
323
|
-
schema: z.object({ image: z.string() }),
|
|
324
|
-
}),
|
|
325
298
|
}
|
|
326
299
|
|
|
327
300
|
export const mediaItemEntrySchema = z.object({
|
|
@@ -340,3 +313,10 @@ export const categoryEntrySchema = z.object({
|
|
|
340
313
|
id: z.string(),
|
|
341
314
|
data: categorySchema,
|
|
342
315
|
})
|
|
316
|
+
|
|
317
|
+
export const mediaCollectionEntrySchema = z.object({
|
|
318
|
+
id: z.string(),
|
|
319
|
+
data: mediaCollectionSchema,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
export type MediaCollectionEntry = z.infer<typeof mediaCollectionEntrySchema>
|
|
@@ -1,43 +1,58 @@
|
|
|
1
1
|
import { AstroError } from "astro/errors"
|
|
2
2
|
import { getCollection } from "astro:content"
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import type { TranslateMapFn } from "../i18n/translate-map"
|
|
5
|
+
import { lazy } from "../utils/lazy"
|
|
5
6
|
import { verifySchemaAsync } from "../utils/verify-schema"
|
|
6
7
|
import { categoryEntrySchema } from "./content-schema"
|
|
7
8
|
import { getMediaItems } from "./get-media-items"
|
|
8
9
|
|
|
9
|
-
const categoriesById =
|
|
10
|
-
|
|
10
|
+
const categoriesById = lazy(async () =>
|
|
11
|
+
Object.fromEntries(
|
|
12
|
+
(await loadCategories()).map(({ id, data }) => [id, data]),
|
|
13
|
+
),
|
|
11
14
|
)
|
|
12
15
|
|
|
13
|
-
const contentCategories =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
const contentCategories = lazy(async () => {
|
|
17
|
+
const categories = await categoriesById.get()
|
|
18
|
+
return Object.fromEntries(
|
|
19
|
+
(await getMediaItems())
|
|
20
|
+
.flatMap((item) => item.data.categories ?? [])
|
|
21
|
+
.map((c) => {
|
|
22
|
+
const category = categories[c.id]
|
|
23
|
+
if (!category) {
|
|
24
|
+
throw new AstroError(
|
|
25
|
+
"Missing Category",
|
|
26
|
+
`A media item references a non-existent category: "${c.id}".\n` +
|
|
27
|
+
`To fix this, create a category file at:\n` +
|
|
28
|
+
`src/content/categories/${c.id}.json`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
return [c.id, category]
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
})
|
|
29
35
|
|
|
30
36
|
/**
|
|
31
37
|
* Get categories that are referenced from media items.
|
|
32
38
|
* Adds the translated name of the category and sorts by this name.
|
|
33
39
|
*
|
|
34
40
|
* @param currentLocale current locale
|
|
35
|
-
* @param t translate function
|
|
36
41
|
* @returns categories sorted by labelText
|
|
37
42
|
*/
|
|
38
|
-
export async function getUsedCategories(
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
export async function getUsedCategories(
|
|
44
|
+
currentLocale: string,
|
|
45
|
+
tMap: TranslateMapFn,
|
|
46
|
+
) {
|
|
47
|
+
const categories = await contentCategories.get()
|
|
48
|
+
return [...Object.entries(categories)]
|
|
49
|
+
.map(([id, data]) => ({
|
|
50
|
+
id,
|
|
51
|
+
...data,
|
|
52
|
+
labelText: tMap(data.label, {
|
|
53
|
+
path: ["categories", id, "label"],
|
|
54
|
+
}),
|
|
55
|
+
}))
|
|
41
56
|
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
42
57
|
}
|
|
43
58
|
|
|
@@ -47,17 +62,26 @@ export async function getUsedCategories(currentLocale: string, t: TranslateFn) {
|
|
|
47
62
|
* by media items use `getUsedCategories`.
|
|
48
63
|
*
|
|
49
64
|
* @param currentLocale current locale
|
|
50
|
-
* @param t translate function
|
|
51
65
|
* @returns categories sorted by labelText
|
|
52
66
|
*/
|
|
53
|
-
export async function getCategories(
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
export async function getCategories(
|
|
68
|
+
currentLocale: string,
|
|
69
|
+
tMap: TranslateMapFn,
|
|
70
|
+
) {
|
|
71
|
+
const categories = await categoriesById.get()
|
|
72
|
+
return [...Object.entries(categories)]
|
|
73
|
+
.map(([id, data]) => ({
|
|
74
|
+
id,
|
|
75
|
+
...data,
|
|
76
|
+
labelText: tMap(data.label, {
|
|
77
|
+
path: ["categories", id, "label"],
|
|
78
|
+
}),
|
|
79
|
+
}))
|
|
56
80
|
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
export async function getCategory(id: string) {
|
|
60
|
-
const category = categoriesById[id]
|
|
84
|
+
const category = (await categoriesById.get())[id]
|
|
61
85
|
if (!category) {
|
|
62
86
|
throw new AstroError(
|
|
63
87
|
`Missing category "${id}"`,
|
|
@@ -1,14 +1,35 @@
|
|
|
1
1
|
import { resolveLanguage } from "../i18n/resolve-language"
|
|
2
|
+
import type { TranslateMapFn } from "../i18n/translate-map"
|
|
3
|
+
import { lazy } from "../utils/lazy"
|
|
2
4
|
import { getMediaItems } from "./get-media-items"
|
|
3
5
|
|
|
6
|
+
type ResolvedLanguage = Awaited<ReturnType<typeof resolveLanguage>>
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* Array of distinct content languages.
|
|
6
10
|
*/
|
|
7
|
-
|
|
8
|
-
Object.fromEntries(
|
|
9
|
-
|
|
10
|
-
language
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
11
|
+
const contentLanguages = lazy(async () => {
|
|
12
|
+
const languagesByCode = Object.fromEntries(
|
|
13
|
+
await Promise.all(
|
|
14
|
+
(await getMediaItems()).map(async ({ data: { language } }) => [
|
|
15
|
+
language,
|
|
16
|
+
resolveLanguage(language),
|
|
17
|
+
]),
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
return Object.values(languagesByCode) as ResolvedLanguage[]
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export const getUsedLanguages = async (
|
|
24
|
+
currentLocale: string,
|
|
25
|
+
tMap: TranslateMapFn,
|
|
26
|
+
) => {
|
|
27
|
+
return (await contentLanguages.get())
|
|
28
|
+
.map((language) => ({
|
|
29
|
+
...language,
|
|
30
|
+
labelText: tMap(language.label, {
|
|
31
|
+
path: ["languages", language.code, "label"],
|
|
32
|
+
}),
|
|
33
|
+
}))
|
|
34
|
+
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
35
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { getCollection } from "astro:content"
|
|
2
|
+
|
|
3
|
+
import { lazy } from "../utils/lazy"
|
|
4
|
+
import { verifySchemaAsync } from "../utils/verify-schema"
|
|
5
|
+
import { mediaCollectionEntrySchema } from "./content-schema"
|
|
6
|
+
|
|
7
|
+
const collectionsByMediaItemIds = lazy(async () =>
|
|
8
|
+
(await loadMediaCollections())
|
|
9
|
+
.flatMap((collection) =>
|
|
10
|
+
collection.data.mediaItems.map(({ id }) => [id, collection.id]),
|
|
11
|
+
)
|
|
12
|
+
.reduce(
|
|
13
|
+
(collected, [mediaId, collectionId]) => {
|
|
14
|
+
const collectionIds = collected[mediaId] ?? []
|
|
15
|
+
|
|
16
|
+
if (!collectionIds.includes(collectionId)) {
|
|
17
|
+
collected[mediaId] = [...collectionIds, collectionId]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return collected
|
|
21
|
+
},
|
|
22
|
+
{} as Record<string, string[]>,
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export const getCollectionsForMediaItem = async (mediaId: string) => {
|
|
27
|
+
return (await collectionsByMediaItemIds.get())[mediaId] ?? []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function loadMediaCollections() {
|
|
31
|
+
const mediaCollections: unknown[] = await getCollection("media-collections")
|
|
32
|
+
return await Promise.all(mediaCollections.map(parseMediaCollection))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function parseMediaCollection(item: unknown) {
|
|
36
|
+
return await verifySchemaAsync(
|
|
37
|
+
mediaCollectionEntrySchema,
|
|
38
|
+
item,
|
|
39
|
+
(id) => `Invalid media-collection: ${id}`,
|
|
40
|
+
(id) =>
|
|
41
|
+
`Fix these issues inside "src/content/media-collections/${id}.json":`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -1,18 +1,52 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AstroError } from "astro/errors"
|
|
2
|
+
import { getCollection } from "astro:content"
|
|
2
3
|
|
|
4
|
+
import type { TranslateMapFn } from "../i18n/translate-map"
|
|
5
|
+
import { lazy } from "../utils/lazy"
|
|
3
6
|
import { verifySchema } from "../utils/verify-schema"
|
|
4
7
|
import { mediaTypeEntrySchema } from "./content-schema"
|
|
8
|
+
import { getMediaItems } from "./get-media-items"
|
|
9
|
+
|
|
10
|
+
const typesById = lazy(async () =>
|
|
11
|
+
Object.fromEntries((await loadTypes()).map((type) => [type.id, type])),
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const contentTypes = lazy(
|
|
15
|
+
async () => new Set((await getMediaItems()).map((item) => item.data.type.id)),
|
|
16
|
+
)
|
|
5
17
|
|
|
6
18
|
export const getMediaType = async (id: string) => {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
const type = (await typesById.get())[id]
|
|
20
|
+
if (!type) {
|
|
21
|
+
throw new AstroError(
|
|
22
|
+
`No media type found for id ${id}`,
|
|
23
|
+
`Fix this by adding a media type definition at "src/content/media-types/${id}.json"`,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return type
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const getUsedMediaTypes = async (
|
|
30
|
+
currentLocale: string,
|
|
31
|
+
tMap: TranslateMapFn,
|
|
32
|
+
) => {
|
|
33
|
+
const byId = await typesById.get()
|
|
34
|
+
return Array.from(await contentTypes.get(), (typeId) => byId[typeId])
|
|
35
|
+
.map(({ id, data }) => ({
|
|
36
|
+
id,
|
|
37
|
+
...data,
|
|
38
|
+
labelText: tMap(data.label, {
|
|
39
|
+
path: ["media-types", id, "label"],
|
|
40
|
+
}),
|
|
41
|
+
}))
|
|
42
|
+
.sort((a, b) => a.labelText.localeCompare(b.labelText, currentLocale))
|
|
13
43
|
}
|
|
14
44
|
|
|
15
45
|
export const getMediaTypes = async () => {
|
|
46
|
+
return Object.values(await typesById.get())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function loadTypes() {
|
|
16
50
|
const mediaTypes: unknown[] = await getCollection("media-types")
|
|
17
51
|
return mediaTypes.map((type: unknown) =>
|
|
18
52
|
verifySchema(
|