erudit 3.0.0-dev.20 → 3.0.0-dev.21

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 (89) hide show
  1. package/app/app.vue +2 -0
  2. package/app/assets/icons/cameo-add.svg +3 -0
  3. package/app/assets/icons/diamond.svg +3 -0
  4. package/app/components/Avatar.vue +118 -0
  5. package/app/components/EruditLink.vue +17 -0
  6. package/app/components/SiteMain.vue +4 -4
  7. package/app/components/ads/Ads.vue +1 -3
  8. package/app/components/ads/AdsProviderYandex.vue +59 -22
  9. package/app/components/aside/AsideListItem.vue +21 -4
  10. package/app/components/aside/AsideMinor.vue +1 -1
  11. package/app/components/aside/major/SiteInfo.vue +4 -4
  12. package/app/components/aside/major/panes/Pages.vue +20 -1
  13. package/app/components/aside/major/panes/nav/fnav/FNavSeparator.vue +2 -2
  14. package/app/components/aside/minor/AsideMinorTopLink.vue +3 -4
  15. package/app/components/aside/minor/contributor/AsideMinorContributor.vue +9 -9
  16. package/app/components/aside/minor/topic/AsideMinorTopic.vue +3 -1
  17. package/app/components/aside/minor/topic/TopicContributors.vue +9 -3
  18. package/app/components/aside/minor/topic/TopicNav.vue +1 -1
  19. package/app/components/aside/minor/topic/TopicToc.vue +12 -13
  20. package/app/components/aside/minor/topic/TopicTocItem.vue +1 -14
  21. package/app/components/bitran/BitranContent.vue +0 -1
  22. package/app/components/contributor/ContributorListItem.vue +13 -5
  23. package/app/components/main/MainActionButton.vue +51 -0
  24. package/app/components/main/MainBitranContent.vue +11 -3
  25. package/app/components/main/MainBreadcrumb.vue +2 -6
  26. package/app/components/main/MainSection.vue +58 -0
  27. package/app/components/main/cameo/MainCameo.vue +135 -0
  28. package/app/components/main/cameo/MainCameoData.vue +232 -0
  29. package/app/components/main/content/ContentPopovers.vue +1 -1
  30. package/app/components/main/topic/MainTopic.vue +13 -18
  31. package/app/components/main/topic/TopicPartSwitch.vue +7 -12
  32. package/app/components/preview/PreviewFooterAction.vue +1 -1
  33. package/app/components/sponsor/SponsorTier1.vue +89 -0
  34. package/app/components/sponsor/SponsorTier2.vue +109 -0
  35. package/app/components/tree/TreeItem.vue +8 -4
  36. package/app/composables/asset.ts +12 -0
  37. package/app/composables/contentData.ts +1 -1
  38. package/app/composables/head.ts +24 -0
  39. package/app/composables/majorPane.ts +1 -0
  40. package/app/composables/url.ts +17 -7
  41. package/app/pages/contributor/[contributorId].vue +9 -6
  42. package/app/pages/contributors.vue +73 -72
  43. package/app/pages/group/[...groupId].vue +4 -3
  44. package/app/pages/sponsors.vue +95 -0
  45. package/app/plugins/prerender.server.ts +14 -2
  46. package/app/scripts/og.ts +2 -1
  47. package/const.ts +0 -1
  48. package/globals/cameo.ts +5 -0
  49. package/globals/register.ts +5 -0
  50. package/globals/sponsor.ts +17 -0
  51. package/languages/en.ts +8 -3
  52. package/languages/ru.ts +8 -3
  53. package/module/imports.ts +13 -6
  54. package/nuxt.config.ts +2 -7
  55. package/package.json +4 -4
  56. package/server/api/cameo/data/[cameoId].ts +42 -0
  57. package/server/api/cameo/ids.ts +5 -0
  58. package/server/api/prerender/assets/cameo.ts +14 -0
  59. package/server/api/prerender/assets/contributor.ts +12 -0
  60. package/server/api/prerender/assets/sponsor.ts +13 -0
  61. package/server/api/prerender/cameos.ts +12 -0
  62. package/server/api/{prerender.ts → prerender/language.ts} +3 -13
  63. package/server/api/sponsor/cameo/data/[sponsorId].ts +51 -0
  64. package/server/api/sponsor/cameo/ids.ts +5 -0
  65. package/server/api/sponsor/count.ts +5 -0
  66. package/server/api/sponsor/list.ts +36 -0
  67. package/server/plugin/build/process.ts +2 -0
  68. package/server/plugin/build/rebuild.ts +2 -0
  69. package/server/plugin/global.ts +2 -0
  70. package/server/plugin/repository/cameo.ts +16 -0
  71. package/server/plugin/repository/contributor.ts +35 -4
  72. package/server/plugin/sponsor/build.ts +82 -0
  73. package/server/plugin/sponsor/index.ts +5 -0
  74. package/server/plugin/sponsor/repository.ts +56 -0
  75. package/server/routes/asset/[...assetPath].ts +34 -0
  76. package/server/routes/robots.txt.ts +9 -0
  77. package/server/routes/sitemap.xml.ts +103 -0
  78. package/shared/asset.ts +0 -5
  79. package/shared/contributor.ts +1 -0
  80. package/shared/link.ts +4 -4
  81. package/shared/types/language.ts +6 -1
  82. package/test/utils/url.test.ts +99 -0
  83. package/utils/ext.ts +41 -0
  84. package/utils/url.ts +23 -0
  85. package/app/components/contributor/ContributorAvatar.vue +0 -45
  86. package/app/components/main/content/ContentSection.vue +0 -45
  87. package/app/public/user.svg +0 -10
  88. package/app/scripts/aside/minor/topic.ts +0 -3
  89. package/utils/slash.ts +0 -11
@@ -14,8 +14,20 @@ export default defineNuxtPlugin({
14
14
 
15
15
  isPrerendering = true;
16
16
 
17
- const routesToPrerender = await $fetch<string[]>('/api/prerender');
18
- _nuxt.runWithContext(() => prerenderRoutes(routesToPrerender));
17
+ const routeProviders = [
18
+ '/api/prerender/language',
19
+ '/api/prerender/cameos',
20
+
21
+ // Assets
22
+ '/api/prerender/assets/cameo',
23
+ '/api/prerender/assets/contributor',
24
+ '/api/prerender/assets/sponsor',
25
+ ];
26
+
27
+ for (const provider of routeProviders) {
28
+ const routes = await $fetch<string[]>(provider);
29
+ _nuxt.runWithContext(() => prerenderRoutes(routes));
30
+ }
19
31
 
20
32
  alreadyPrerendered = true;
21
33
  },
package/app/scripts/og.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import eruditConfig from '#erudit/config';
2
2
  import type { ImageData } from '@erudit/shared/image';
3
+ import { normalizeUrl } from '@erudit/utils/url';
3
4
 
4
5
  export const defaultOgImage = eruditConfig.seo?.defaultOgImage || {
5
6
  src: eruditAsset('og-default.png'),
@@ -13,7 +14,7 @@ export function createOgImageTags(siteUrl: string, data: ImageData | string) {
13
14
 
14
15
  tags.push({
15
16
  name: 'og:image',
16
- content: siteUrl + (isImage ? data.src : data),
17
+ content: normalizeUrl(siteUrl + (isImage ? data.src : data)),
17
18
  });
18
19
 
19
20
  if (isImage) {
package/const.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export const PUBLIC_ERUDIT_ASSET = '/erudit-asset/';
2
- export const PUBLIC_CONTRIBUTOR_ASSET = '/user-asset/contributor/';
3
2
  export const PUBLIC_CONTENT_ASSET = '/user-asset/content/';
4
3
  export const PUBLIC_USER_ASSET = '/user-asset/public/';
@@ -0,0 +1,5 @@
1
+ import type { CameoConfig } from '@erudit-js/cog/schema';
2
+
3
+ export function defineCameo(cameoConfig: CameoConfig) {
4
+ return cameoConfig;
5
+ }
@@ -2,6 +2,9 @@ import * as erudit from './erudit';
2
2
  import * as contributor from './contributor';
3
3
  import * as bitran from './bitran';
4
4
  import * as content from './content';
5
+ import * as cameo from './cameo';
6
+ import * as sponsor from './sponsor';
7
+
5
8
  import * as asset from '@shared/asset';
6
9
 
7
10
  export function registerGlobals() {
@@ -11,6 +14,8 @@ export function registerGlobals() {
11
14
  ...bitran,
12
15
  ...content,
13
16
  ...asset,
17
+ ...cameo,
18
+ ...sponsor,
14
19
  })) {
15
20
  // @ts-ignore
16
21
  globalThis[key] = value;
@@ -0,0 +1,17 @@
1
+ import type {
2
+ SponsorConfig,
3
+ Tier1Sponsor,
4
+ Tier2Sponsor,
5
+ } from '@erudit-js/cog/schema';
6
+
7
+ export function defineTier1Sponsor(
8
+ sponsorConfig: SponsorConfig<Tier1Sponsor>,
9
+ ): SponsorConfig<Tier1Sponsor> {
10
+ return sponsorConfig;
11
+ }
12
+
13
+ export function defineTier2Sponsor(
14
+ sponsorConfig: SponsorConfig<Tier2Sponsor>,
15
+ ): SponsorConfig<Tier2Sponsor> {
16
+ return sponsorConfig;
17
+ }
package/languages/en.ts CHANGED
@@ -29,8 +29,9 @@ const english: EruditPhrases = {
29
29
  contributors: 'Contributors',
30
30
  contributors_page_description:
31
31
  'List of people who contributed to the project materials: suggested valuable ideas, made corrections to existing material or wrote their own!',
32
- contributors_page_invite: (link) =>
33
- `You can also help the project, <a href="${link}" target="_blank">make a contribution</a> and get on this page!`,
32
+ contributors_page_invite:
33
+ 'You can also help the project, make a contribution and get on this page!',
34
+ become_contributor: 'Become a contributor',
34
35
  contributor: 'Contributor',
35
36
  contribution: 'Contribution',
36
37
  contributions_explain: (count) =>
@@ -87,7 +88,11 @@ const english: EruditPhrases = {
87
88
  references: 'References',
88
89
  reference_source_featured: 'Featured source',
89
90
  references_description:
90
- 'List of external sources that were used in writing this material. For a deeper dive into the material, it is recommended to read them in more detail, especially the featured sources, which are marked with an asterisk:',
91
+ 'A list of external sources that were used to write this material. If there is an asterisk next to the title, it is a featured source and is worth reading if you want to delve deeper into the material.',
92
+ sponsors: 'Sponsors',
93
+ sponsors_description:
94
+ 'People and organizations that support the project financially. Thanks to them, we can continue to develop the project and improve the quality of materials. If you want to support the project, you can become a sponsor too!',
95
+ become_sponsor: 'Become a sponsor',
91
96
  };
92
97
 
93
98
  export default english;
package/languages/ru.ts CHANGED
@@ -29,8 +29,9 @@ const russian: EruditPhrases = {
29
29
  contributors: 'Авторы',
30
30
  contributors_page_description:
31
31
  'Список людей, которые внесли вклад в материалы проекта: предложили ценные идеи, вносили корректировки в существующий материал или же написали собственный!',
32
- contributors_page_invite: (link) =>
33
- `Вы тоже можете помочь проекту, <a href="${link}" target="_blank">внести свой вклад</a> и попасть на эту страницу!`,
32
+ contributors_page_invite:
33
+ 'Вы тоже можете помочь проекту, внести свой вклад и попасть на эту страницу!',
34
+ become_contributor: 'Стать автором',
34
35
  contributor: 'Автор',
35
36
  contribution: 'Вклад',
36
37
  contributions_explain: (count) =>
@@ -88,7 +89,11 @@ const russian: EruditPhrases = {
88
89
  references: 'Источники',
89
90
  reference_source_featured: 'Избранный источник',
90
91
  references_description:
91
- 'Список внешних источников, которые использовались при написании этого материала. Для более глубокого погружения в материал рекомендуются ознакомиться с ними подробнее, особенно с избранными источниками, которые отмечены звездочкой:',
92
+ 'Список внешних источников, которые использовались при написании этого материала. Если рядом с названием стоит звездочка, то это избранный источник и с ним стоит ознакомиться, если вы хотите глубже погрузиться в материал.',
93
+ sponsors: 'Спонсоры',
94
+ sponsors_description:
95
+ 'Список людей и организаций, которые поддерживают проект финансово. Благодаря им проект может существовать и развиваться. Если вы хотите помочь проекту, то тоже можете стать спонсором и попасть на эту страницу!',
96
+ become_sponsor: 'Стать спонсором',
92
97
  };
93
98
 
94
99
  export default russian;
package/module/imports.ts CHANGED
@@ -12,6 +12,18 @@ export function setupGlobalImports() {
12
12
  name: 'defineContributor',
13
13
  from: eruditPath('globals/contributor'),
14
14
  },
15
+ {
16
+ name: 'defineCameo',
17
+ from: eruditPath('globals/cameo'),
18
+ },
19
+ {
20
+ name: 'defineTier1Sponsor',
21
+ from: eruditPath('globals/sponsor'),
22
+ },
23
+ {
24
+ name: 'defineTier2Sponsor',
25
+ from: eruditPath('globals/sponsor'),
26
+ },
15
27
  // Bitran
16
28
  ...(() => {
17
29
  const imports = ['defineAppBitran'];
@@ -41,12 +53,7 @@ export function setupGlobalImports() {
41
53
  },
42
54
  // Helper Asset Path Functions
43
55
  ...(() => {
44
- const imports = [
45
- 'eruditAsset',
46
- 'contributorAsset',
47
- 'contentAsset',
48
- 'publicAsset',
49
- ];
56
+ const imports = ['eruditAsset', 'contentAsset', 'publicAsset'];
50
57
  return imports.map((name) => ({
51
58
  name,
52
59
  from: eruditPath(`shared/asset`),
package/nuxt.config.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  PUBLIC_CONTENT_ASSET,
3
- PUBLIC_CONTRIBUTOR_ASSET,
4
3
  PUBLIC_ERUDIT_ASSET,
5
4
  PUBLIC_USER_ASSET,
6
5
  } from './const';
@@ -57,9 +56,10 @@ export default defineNuxtConfig({
57
56
  crawlLinks: true,
58
57
  failOnError: false,
59
58
  concurrency: 10,
59
+ routes: ['/robots.txt', '/sitemap.xml'],
60
60
  ignore: [
61
61
  (path: string) => {
62
- const regexps = [/\?element=/gm, /#/gm, /\.json\?/gm];
62
+ const regexps = [/#/gm, /\.json\?/gm];
63
63
  const shouldIgnore = regexps.some((re) => re.test(path));
64
64
  return shouldIgnore;
65
65
  },
@@ -87,11 +87,6 @@ export default defineNuxtConfig({
87
87
  dir: projectPath('content'),
88
88
  maxAge: 60 * 60 * 24 * 30,
89
89
  },
90
- {
91
- baseURL: PUBLIC_CONTRIBUTOR_ASSET,
92
- dir: projectPath('contributors'),
93
- maxAge: 60 * 60 * 24 * 30,
94
- },
95
90
  ],
96
91
  esbuild: {
97
92
  options: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "3.0.0-dev.20",
3
+ "version": "3.0.0-dev.21",
4
4
  "type": "module",
5
5
  "description": "🤓 CMS for perfect educational sites.",
6
6
  "license": "MIT",
@@ -15,9 +15,9 @@
15
15
  "erudit": "bin/erudit.mjs"
16
16
  },
17
17
  "peerDependencies": {
18
- "@erudit-js/cog": "3.0.0-dev.20",
19
- "@erudit-js/cli": "3.0.0-dev.20",
20
- "@erudit-js/bitran-elements": "3.0.0-dev.20"
18
+ "@erudit-js/cog": "3.0.0-dev.21",
19
+ "@erudit-js/cli": "3.0.0-dev.21",
20
+ "@erudit-js/bitran-elements": "3.0.0-dev.21"
21
21
  },
22
22
  "dependencies": {
23
23
  "@bitran-js/core": "1.0.0-dev.13",
@@ -0,0 +1,42 @@
1
+ import { glob } from 'glob';
2
+ import { type Cameo } from '@erudit-js/cog/schema';
3
+
4
+ import { PROJECT_DIR } from '@erudit/globalPath';
5
+ import { IMPORT } from '@server/importer';
6
+
7
+ export default defineEventHandler<Promise<Cameo>>(async (event) => {
8
+ const cameoId = event.context.params?.cameoId;
9
+
10
+ if (typeof cameoId !== 'string' || !cameoId) {
11
+ throw createError({
12
+ statusCode: 400,
13
+ message: 'Cameo ID is required and must be a string!',
14
+ });
15
+ }
16
+
17
+ const avatarPaths = await glob(`cameos/${cameoId}/avatars/*`, {
18
+ cwd: PROJECT_DIR,
19
+ absolute: false,
20
+ posix: true,
21
+ });
22
+
23
+ const avatarRoutes = avatarPaths.map((path) => `/asset/${path}`);
24
+
25
+ let cameoConfig;
26
+ try {
27
+ cameoConfig = await IMPORT(`${PROJECT_DIR}/cameos/${cameoId}/cameo`, {
28
+ default: true,
29
+ });
30
+ } catch (error) {
31
+ throw createError({
32
+ statusCode: 500,
33
+ message: `Failed to import config for ID "${cameoId}"! Error: ${error}`,
34
+ });
35
+ }
36
+
37
+ return {
38
+ ...cameoConfig,
39
+ cameoId,
40
+ avatars: avatarRoutes,
41
+ } satisfies Cameo;
42
+ });
@@ -0,0 +1,5 @@
1
+ import { getCameoIds } from '@server/repository/cameo';
2
+
3
+ export default defineEventHandler(async () => {
4
+ return await getCameoIds();
5
+ });
@@ -0,0 +1,14 @@
1
+ import { PROJECT_DIR } from '#erudit/globalPaths';
2
+ import { glob } from 'glob';
3
+
4
+ const includePatterns = [`cameos/*/avatars/*`];
5
+
6
+ export default defineEventHandler(async () => {
7
+ const files = await glob(includePatterns, {
8
+ cwd: PROJECT_DIR,
9
+ absolute: false,
10
+ posix: true,
11
+ });
12
+
13
+ return files.map((file) => '/asset/' + file);
14
+ });
@@ -0,0 +1,12 @@
1
+ import { glob } from 'glob';
2
+ import { PROJECT_DIR } from '#erudit/globalPaths';
3
+
4
+ export default defineEventHandler(async () => {
5
+ const avatars = await glob('contributors/*/avatar.*', {
6
+ cwd: PROJECT_DIR,
7
+ absolute: false,
8
+ posix: true,
9
+ });
10
+
11
+ return avatars.map((avatar) => '/asset/' + avatar);
12
+ });
@@ -0,0 +1,13 @@
1
+ import { ERUDIT_SERVER } from '@server/global';
2
+
3
+ export default defineEventHandler(async () => {
4
+ const serverSponsors = ERUDIT_SERVER.SPONSORS;
5
+
6
+ if (!serverSponsors) {
7
+ return [];
8
+ }
9
+
10
+ return Object.values(serverSponsors.avatars)
11
+ .filter(Boolean)
12
+ .map((avatar) => '/asset/' + avatar);
13
+ });
@@ -0,0 +1,12 @@
1
+ import { getCameoIds } from '@server/repository/cameo';
2
+ import { getTier2SponsorIds } from '@server/sponsor/repository';
3
+
4
+ export default defineEventHandler(async () => {
5
+ const cameoIds = await getCameoIds();
6
+ return [
7
+ '/api/cameo/ids',
8
+ '/api/sponsor/cameo/ids',
9
+ ...cameoIds.map((id) => `/api/cameo/data/${id}`),
10
+ ...getTier2SponsorIds().map((id) => `/api/sponsor/cameo/data/${id}`),
11
+ ];
12
+ });
@@ -1,19 +1,9 @@
1
1
  import { ERUDIT_SERVER } from '@server/global';
2
2
 
3
- export default defineEventHandler(async () => {
4
- const routes: string[] = [];
5
- routes.push(...language());
6
-
7
- return routes;
8
- });
9
-
10
- //
11
- //
12
- //
13
-
14
- function language() {
3
+ export default defineEventHandler(() => {
15
4
  const phraseRoutes = Object.keys(ERUDIT_SERVER.LANGUAGE?.phrases || []).map(
16
5
  (phraseId) => `/api/language/phrase/${phraseId}`,
17
6
  );
7
+
18
8
  return ['/api/language/functions', ...phraseRoutes];
19
- }
9
+ });
@@ -0,0 +1,51 @@
1
+ import { type Cameo, type Sponsor } from '@erudit-js/cog/schema';
2
+
3
+ import { retrieveSponsor } from '@server/sponsor/repository';
4
+ import { ERUDIT_SERVER } from '@server/global';
5
+
6
+ export default defineEventHandler<Promise<Cameo>>(async (event) => {
7
+ const sponsorId = event.context.params?.sponsorId;
8
+
9
+ if (typeof sponsorId !== 'string' || !sponsorId) {
10
+ throw createError({
11
+ statusCode: 400,
12
+ message: 'Sponsor ID is required and must be a string!',
13
+ });
14
+ }
15
+
16
+ let sponsor: Sponsor;
17
+ try {
18
+ sponsor = await retrieveSponsor(sponsorId);
19
+ } catch (error) {
20
+ throw createError({
21
+ statusCode: 500,
22
+ message: `Failed to retrieve sponsor with ID "${sponsorId}"! Error: ${error}`,
23
+ });
24
+ }
25
+
26
+ if (sponsor.tier !== 'tier2') {
27
+ throw createError({
28
+ statusCode: 404,
29
+ message: `Sponsor with ID "${sponsorId}" is not a Tier 2 sponsor!`,
30
+ });
31
+ }
32
+
33
+ const defaultMessages = ERUDIT_SERVER.CONFIG.sponsors?.defaultCameoMessages;
34
+
35
+ if (!defaultMessages) {
36
+ throw createError({
37
+ statusCode: 500,
38
+ message: 'Default sponsor cameo messages are not configured!',
39
+ });
40
+ }
41
+
42
+ return {
43
+ cameoId: sponsorId,
44
+ name: sponsor.name,
45
+ icon: sponsor.icon,
46
+ avatars: [sponsor.avatar],
47
+ color: sponsor.color,
48
+ link: sponsor.link,
49
+ messages: [...(sponsor.messages || []), ...defaultMessages],
50
+ } satisfies Cameo;
51
+ });
@@ -0,0 +1,5 @@
1
+ import { getTier2SponsorIds } from '@server/sponsor/repository';
2
+
3
+ export default defineEventHandler(async () => {
4
+ return getTier2SponsorIds();
5
+ });
@@ -0,0 +1,5 @@
1
+ import { getSponsorCount } from '@server/sponsor/repository';
2
+
3
+ export default defineEventHandler(async () => {
4
+ return getSponsorCount();
5
+ });
@@ -0,0 +1,36 @@
1
+ import type { Tier1Sponsor, Tier2Sponsor } from '@erudit-js/cog/schema';
2
+
3
+ import { getSponsorIds, retrieveSponsor } from '@server/sponsor/repository';
4
+
5
+ const BATCH_SIZE = 10;
6
+
7
+ export default defineEventHandler(async () => {
8
+ const sponsorIds = getSponsorIds();
9
+ const sponsors = await processSponsorsBatched(sponsorIds);
10
+ return categorizeSponsorsByTier(sponsors);
11
+ });
12
+
13
+ async function processSponsorsBatched(sponsorIds: string[]) {
14
+ const allSponsors = [];
15
+
16
+ for (let i = 0; i < sponsorIds.length; i += BATCH_SIZE) {
17
+ const batch = sponsorIds.slice(i, i + BATCH_SIZE);
18
+ const batchResults = await Promise.all(
19
+ batch.map((sponsorId) => retrieveSponsor(sponsorId!)),
20
+ );
21
+ allSponsors.push(...batchResults);
22
+ }
23
+
24
+ return allSponsors;
25
+ }
26
+
27
+ function categorizeSponsorsByTier(sponsors: (Tier1Sponsor | Tier2Sponsor)[]) {
28
+ return {
29
+ tier1: sponsors.filter(
30
+ (sponsor): sponsor is Tier1Sponsor => sponsor.tier === 'tier1',
31
+ ),
32
+ tier2: sponsors.filter(
33
+ (sponsor): sponsor is Tier2Sponsor => sponsor.tier === 'tier2',
34
+ ),
35
+ };
36
+ }
@@ -7,6 +7,7 @@ import { setupLanguage } from './jobs/language';
7
7
  import { buildContributors } from './jobs/contributors';
8
8
  import { buildNav } from './jobs/nav';
9
9
  import { buildContent } from './jobs/content/generic';
10
+ import { buildSponsors } from '@server/sponsor/build';
10
11
 
11
12
  let initial = true;
12
13
 
@@ -23,6 +24,7 @@ async function _build() {
23
24
  await buildContributors();
24
25
  await buildNav();
25
26
  await buildContent();
27
+ await buildSponsors();
26
28
 
27
29
  logger.success('Build successful!');
28
30
  }
@@ -13,6 +13,8 @@ const watchTargets: string[] = [
13
13
  PROJECT_DIR + '/content',
14
14
  // Contributors directory
15
15
  PROJECT_DIR + '/contributors',
16
+ // Sponsors directory
17
+ PROJECT_DIR + '/sponsors',
16
18
  ];
17
19
 
18
20
  const ignoreTargets: string[] = [];
@@ -3,6 +3,7 @@ import type { EruditConfig } from '@erudit-js/cog/schema';
3
3
 
4
4
  import type { EruditPhrases } from '@shared/types/language';
5
5
  import type { NavNode, RootNavNode } from '@server/nav/node';
6
+ import type { ServerSponsors } from '@server/sponsor';
6
7
 
7
8
  interface EruditServer {
8
9
  BUILD_PROMISE: Promise<void>;
@@ -11,6 +12,7 @@ interface EruditServer {
11
12
  DB: DataSource;
12
13
  NAV?: RootNavNode;
13
14
  NAV_BOOKS?: Record<string, NavNode>;
15
+ SPONSORS?: ServerSponsors;
14
16
  }
15
17
 
16
18
  export const ERUDIT_SERVER: EruditServer = {} as any;
@@ -0,0 +1,16 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { PROJECT_DIR } from '#erudit/globalPaths';
3
+
4
+ export async function getCameoIds() {
5
+ try {
6
+ const dirents = await readdir(`${PROJECT_DIR}/cameos`, {
7
+ withFileTypes: true,
8
+ });
9
+
10
+ return dirents
11
+ .filter((dirent) => dirent.isDirectory())
12
+ .map((dirent) => dirent.name);
13
+ } catch {
14
+ return [];
15
+ }
16
+ }
@@ -62,13 +62,20 @@ export async function getContributions(
62
62
  );
63
63
 
64
64
  const contributions: Contribution[] = [];
65
+ const existingBookTitles = new Set<string>();
65
66
 
66
67
  for (const dbContribution of dbContributions) {
67
68
  const contentId = dbContribution.contentId;
68
69
  const contentNavNode = getNavNode(contentId);
69
70
 
70
- const bookTitle = await (async () => {
71
- const bookId = getNavBookOf(contentId)?.fullId;
71
+ const bookData = await (async () => {
72
+ const bookNavNode = getNavBookOf(contentId);
73
+
74
+ if (!bookNavNode) {
75
+ return undefined;
76
+ }
77
+
78
+ const bookId = bookNavNode.fullId;
72
79
 
73
80
  if (!bookId) {
74
81
  return undefined;
@@ -78,14 +85,38 @@ export async function getContributions(
78
85
  return undefined;
79
86
  }
80
87
 
81
- return await getBookTitleFor(bookId);
88
+ let bookTitle = await getBookTitleFor(bookId);
89
+
90
+ let cursor = bookNavNode;
91
+ while (cursor) {
92
+ if (cursor.type === 'group') {
93
+ const groupTitle = await getContentTitle(cursor.fullId);
94
+ const newBookTitle = groupTitle + ' / ' + bookTitle;
95
+
96
+ if (bookTitle && existingBookTitles.has(bookTitle)) {
97
+ bookTitle = newBookTitle;
98
+ }
99
+ }
100
+
101
+ cursor = cursor.parent as any;
102
+ }
103
+
104
+ if (bookTitle) {
105
+ existingBookTitles.add(bookTitle);
106
+ }
107
+
108
+ return {
109
+ bookId,
110
+ bookTitle,
111
+ };
82
112
  })();
83
113
 
84
114
  const contentTitle = await getContentTitle(contentId);
85
115
  const contentLink = await getContentLink(contentId);
86
116
 
87
117
  const contribution: Contribution = {
88
- bookTitle: bookTitle,
118
+ bookId: bookData?.bookId,
119
+ bookTitle: bookData?.bookTitle,
89
120
  contentType: contentNavNode.type,
90
121
  contentTitle,
91
122
  contentLink,