erudit 3.0.0-dev.21 → 3.0.0-dev.23

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 (72) hide show
  1. package/app/assets/icons/cameo-add.svg +3 -3
  2. package/app/assets/icons/diamond.svg +2 -2
  3. package/app/assets/icons/files.svg +5 -0
  4. package/app/assets/icons/list-squared.svg +3 -0
  5. package/app/assets/icons/rocket.svg +1 -0
  6. package/app/components/GroupLikePage.vue +70 -0
  7. package/app/components/QuickLinks.vue +99 -0
  8. package/app/components/aside/major/SiteInfo.vue +2 -2
  9. package/app/components/aside/major/panes/nav/NavBook.vue +1 -1
  10. package/app/components/aside/major/panes/other/ItemTheme.vue +25 -30
  11. package/app/components/index/IndexAvatars.vue +143 -0
  12. package/app/components/main/MainActionButton.vue +3 -1
  13. package/app/components/main/MainBitranContent.vue +13 -0
  14. package/app/components/main/MainDescription.vue +6 -0
  15. package/app/components/main/MainSectionTitle.vue +50 -0
  16. package/app/components/main/MainSourceUsages.vue +119 -0
  17. package/app/components/main/MainSourcesUsage.vue +60 -0
  18. package/app/components/main/MainToc.vue +135 -0
  19. package/app/components/main/content/ContentPopovers.vue +9 -2
  20. package/app/components/main/content/ContentReferences.vue +1 -27
  21. package/app/components/main/content/reference/ReferenceGroup.vue +10 -9
  22. package/app/components/main/content/reference/ReferenceSource.vue +1 -0
  23. package/app/components/main/topic/MainTopic.vue +5 -8
  24. package/app/components/main/topic/MainTopicQuickLinks.vue +24 -0
  25. package/app/components/stats/Stats.vue +21 -0
  26. package/app/components/stats/StatsGroupLike.vue +24 -0
  27. package/app/components/stats/StatsItem.vue +33 -0
  28. package/app/components/stats/StatsItemElement.vue +13 -0
  29. package/app/composables/bitranLocation.ts +2 -3
  30. package/app/composables/contentPage.ts +7 -6
  31. package/app/composables/theme.ts +26 -5
  32. package/app/pages/book/[...bookId].vue +6 -31
  33. package/app/pages/group/[...groupId].vue +5 -41
  34. package/app/pages/index.vue +189 -16
  35. package/app/pages/sponsors.vue +2 -2
  36. package/app/scripts/preview/data/pageLink.ts +4 -3
  37. package/app/scripts/preview/data/unique.ts +6 -6
  38. package/languages/en.ts +7 -1
  39. package/languages/ru.ts +10 -3
  40. package/package.json +4 -4
  41. package/server/api/content/data.ts +33 -11
  42. package/server/api/index/data.ts +46 -0
  43. package/server/plugin/bitran/content.ts +0 -14
  44. package/server/plugin/bitran/location.ts +3 -6
  45. package/server/plugin/build/jobs/content/parse.ts +95 -1
  46. package/server/plugin/build/jobs/content/type/group.ts +0 -21
  47. package/server/plugin/content/context.ts +3 -6
  48. package/server/plugin/db/entities/Group.ts +0 -3
  49. package/server/plugin/db/entities/QuickLink.ts +19 -0
  50. package/server/plugin/db/entities/Stat.ts +13 -0
  51. package/server/plugin/db/setup.ts +4 -0
  52. package/server/plugin/repository/content.ts +3 -3
  53. package/server/plugin/repository/contentToc.ts +90 -0
  54. package/server/plugin/repository/elementStats.ts +80 -0
  55. package/server/plugin/repository/link.ts +20 -0
  56. package/server/plugin/repository/quickLink.ts +36 -0
  57. package/server/plugin/repository/readLink.ts +17 -0
  58. package/server/plugin/repository/reference.ts +78 -0
  59. package/server/plugin/repository/topicCount.ts +19 -0
  60. package/shared/content/data/base.ts +2 -2
  61. package/shared/content/data/groupLike.ts +11 -0
  62. package/shared/content/data/type/book.ts +3 -1
  63. package/shared/content/data/type/group.ts +3 -1
  64. package/shared/content/data/type/topic.ts +3 -1
  65. package/shared/content/reference.ts +18 -0
  66. package/shared/content/toc.ts +35 -0
  67. package/shared/indexData.ts +10 -0
  68. package/shared/link.ts +3 -2
  69. package/shared/quickLink.ts +7 -0
  70. package/shared/stat.ts +23 -0
  71. package/shared/types/language.ts +6 -1
  72. package/utils/normalize.ts +7 -0
@@ -1,14 +1,29 @@
1
1
  <script lang="ts" setup>
2
2
  import eruditConfig from '#erudit/config';
3
+ import { type IndexData } from '@shared/indexData';
4
+ import { normalizeText } from '@erudit/utils/normalize';
3
5
 
4
- const phrase = await usePhrases('seo_index_title', 'seo_index_description');
6
+ const pretty = useFormatText();
5
7
 
6
- const seoTitle =
8
+ const phrase = await usePhrases(
9
+ 'seo_index_title',
10
+ 'seo_index_description',
11
+ 'topics',
12
+ 'x_contributors',
13
+ 'x_sponsors',
14
+ );
15
+
16
+ const seoTitle = pretty(
7
17
  eruditConfig.seo?.indexTitle ||
8
- eruditConfig.seo?.title ||
9
- phrase.seo_index_title;
10
- const seoDescription =
11
- eruditConfig.seo?.indexDescription || phrase.seo_index_description;
18
+ eruditConfig.seo?.title ||
19
+ phrase.seo_index_title,
20
+ );
21
+
22
+ const seoDescription = pretty(
23
+ normalizeText(
24
+ eruditConfig.seo?.indexDescription || phrase.seo_index_description,
25
+ ),
26
+ );
12
27
 
13
28
  useSeoMeta({
14
29
  title: seoTitle,
@@ -16,17 +31,175 @@ useSeoMeta({
16
31
  description: seoDescription,
17
32
  ogDescription: seoDescription,
18
33
  });
34
+
35
+ const { data: indexData } = (await useAsyncData('index', () =>
36
+ $fetch('/api/index/data'),
37
+ )) as { data: Ref<IndexData> };
38
+
39
+ const logotype = computed(() => {
40
+ return eruditConfig.index?.logotype
41
+ ? {
42
+ src: eruditConfig.index.logotype.src,
43
+ maxWidth: eruditConfig.index.logotype.maxWidth || '100%',
44
+ invert: eruditConfig.index.logotype.invert,
45
+ }
46
+ : undefined;
47
+ });
48
+
49
+ const title = computed(() => {
50
+ return pretty(
51
+ eruditConfig.index?.title ||
52
+ eruditConfig.seo?.title ||
53
+ eruditConfig.site?.title ||
54
+ phrase.seo_index_title,
55
+ );
56
+ });
57
+
58
+ const slogan = computed(() => {
59
+ return eruditConfig.index?.slogan;
60
+ });
61
+
62
+ const canShowStats = computed(() => {
63
+ const hasElementStats =
64
+ indexData.value.elementStats && indexData.value.elementStats.length > 0;
65
+
66
+ const hasTopics =
67
+ indexData.value.topicCount && indexData.value.topicCount > 0;
68
+
69
+ return hasElementStats || hasTopics;
70
+ });
71
+
72
+ const description = computed(() => {
73
+ return eruditConfig.index?.description;
74
+ });
75
+
76
+ const canShowParticipants = computed(() => {
77
+ return (
78
+ indexData.value.contributors.length > 0 ||
79
+ indexData.value.sponsors.length > 0
80
+ );
81
+ });
19
82
  </script>
20
83
 
21
84
  <template>
22
- <div style="padding: var(--_pMainY) var(--_pMainX)">
23
- <h1>
24
- {{
25
- eruditConfig.seo?.title ||
26
- eruditConfig.site?.title ||
27
- phrase.seo_index_title
28
- }}
29
- </h1>
30
- <p>TODO</p>
31
- </div>
85
+ <header :class="$style.indexHeader">
86
+ <img
87
+ v-if="logotype"
88
+ :src="logotype.src"
89
+ :style="{ maxWidth: logotype.maxWidth }"
90
+ :class="[
91
+ $style.logotype,
92
+ logotype.invert === 'dark' ? $style.invertDark : '',
93
+ logotype.invert === 'light' ? $style.invertLight : '',
94
+ ]"
95
+ />
96
+ <h1 :class="$style.title">{{ pretty(title) }}</h1>
97
+ <section v-if="slogan" :class="$style.slogan">
98
+ {{ pretty(slogan) }}
99
+ </section>
100
+ <Stats
101
+ v-if="canShowStats"
102
+ :stats="[
103
+ {
104
+ type: 'custom',
105
+ icon: 'files',
106
+ label: phrase.topics,
107
+ count: indexData.topicCount,
108
+ },
109
+ ...indexData.elementStats,
110
+ ]"
111
+ :class="$style.stats"
112
+ />
113
+ <section v-if="description" :class="$style.description">
114
+ {{ pretty(description) }}
115
+ </section>
116
+ <section v-if="canShowParticipants" :class="$style.participants">
117
+ <IndexAvatars
118
+ v-if="indexData.contributors.length > 0"
119
+ :label="phrase.x_contributors(indexData.contributors.length)"
120
+ link="/contributors/"
121
+ :max="4"
122
+ icon="user"
123
+ :namesAvatars="indexData.contributors"
124
+ />
125
+
126
+ <IndexAvatars
127
+ v-if="indexData.sponsors.length > 0"
128
+ :label="phrase.x_sponsors(indexData.sponsors.length)"
129
+ link="/sponsors/"
130
+ :max="4"
131
+ icon="diamond"
132
+ :namesAvatars="indexData.sponsors"
133
+ />
134
+ </section>
135
+ </header>
136
+ <MainToc v-if="indexData.contentToc" :toc="indexData.contentToc" />
32
137
  </template>
138
+
139
+ <style lang="scss" module>
140
+ @use '$/def/bp';
141
+
142
+ .indexHeader {
143
+ display: flex;
144
+ flex-direction: column;
145
+ gap: calc(1.5 * var(--_pMainY));
146
+ //padding: var(--_pMainY) 5.5vw;
147
+ padding: var(--_pMainY) var(--_pMainX);
148
+
149
+ @include bp.below('mobile') {
150
+ padding: var(--_pMainY) var(--_pMainX);
151
+ }
152
+
153
+ .logotype {
154
+ margin: 0 auto;
155
+ margin-bottom: var(--_pMainY);
156
+ width: 100%;
157
+
158
+ [data-theme='dark'] &.invertDark,
159
+ [data-theme='light'] &.invertLight {
160
+ filter: invert(1) hue-rotate(180deg);
161
+ }
162
+ }
163
+
164
+ .title {
165
+ font-size: clamp(2em, 2vw + 1em, 2.6em);
166
+ text-align: center;
167
+ text-shadow: 3px 3px color-mix(in srgb, var(--brand), transparent 70%);
168
+ line-height: 1;
169
+ }
170
+
171
+ .slogan {
172
+ text-align: center;
173
+ font-weight: 600;
174
+ font-size: 1.4em;
175
+ color: var(--textMuted);
176
+ }
177
+
178
+ .stats {
179
+ font-size: 1.4em;
180
+ gap: var(--gapBig);
181
+ justify-content: center;
182
+
183
+ @include bp.below('mobile') {
184
+ gap: var(--gap);
185
+ }
186
+ }
187
+
188
+ .description {
189
+ font-size: 1.1em;
190
+ //text-align: justify;
191
+ }
192
+
193
+ .participants {
194
+ display: flex;
195
+ flex-wrap: wrap;
196
+ justify-content: space-around;
197
+ align-items: center;
198
+ gap: var(--gapBig);
199
+
200
+ @include bp.below('mobile') {
201
+ gap: var(--gap);
202
+ }
203
+ }
204
+ }
205
+ </style>
@@ -23,10 +23,10 @@ useEruditHead({
23
23
  <MainTitle icon="diamond" :title="phrase.sponsors" />
24
24
  <MainDescription :description="phrase.sponsors_description" />
25
25
  <MainActionButton
26
- v-if="eruditConfig.content?.howToImproveLink"
26
+ v-if="eruditConfig.sponsors?.addLink"
27
27
  icon="diamond"
28
28
  :label="phrase.become_sponsor"
29
- :link="eruditConfig.content.howToImproveLink"
29
+ :link="eruditConfig.sponsors?.addLink"
30
30
  />
31
31
  <MainSection :class="$style.sponsorSection" v-if="sponsors?.tier2?.length">
32
32
  <template v-slot:header>
@@ -1,3 +1,5 @@
1
+ import { trailingSlash } from '@erudit/utils/url';
2
+
1
3
  import { PreviewDataType, type PreviewDataBase } from '../data';
2
4
  import { PreviewRequestType, type PreviewRequest } from '../request';
3
5
  import type { PreviewFooter } from '../footer';
@@ -17,7 +19,6 @@ export async function buildPageLink(
17
19
 
18
20
  if (linkTarget.type !== 'page') return;
19
21
 
20
- return await $fetch(`/api/preview/page${linkTarget._href}`, {
21
- responseType: 'json',
22
- });
22
+ const route = trailingSlash(`/api/preview/page${linkTarget._href}`, false);
23
+ return await $fetch(route, { responseType: 'json' });
23
24
  }
@@ -1,8 +1,6 @@
1
- import {
2
- encodeBitranLocation,
3
- parseBitranLocation,
4
- } from '@erudit-js/cog/schema';
1
+ import { encodeBitranLocation } from '@erudit-js/cog/schema';
5
2
 
3
+ import { trailingSlash } from '@erudit/utils/url';
6
4
  import type { RawBitranContent } from '@shared/bitran/content';
7
5
 
8
6
  import { PreviewDataType, type PreviewDataBase } from '../data';
@@ -26,10 +24,12 @@ export async function buildUnique(
26
24
 
27
25
  if (linkTarget.type !== 'unique') return;
28
26
 
29
- const serverData = await $fetch(
27
+ const route = trailingSlash(
30
28
  `/api/preview/unique/${encodeBitranLocation(linkTarget._absoluteStrLocation!)}`,
31
- { responseType: 'json' },
29
+ false,
32
30
  );
31
+
32
+ const serverData = (await $fetch(route, { responseType: 'json' })) as any;
33
33
  const elementName = serverData.elementName;
34
34
  const customTitle = serverData.title;
35
35
 
package/languages/en.ts CHANGED
@@ -63,9 +63,10 @@ const english: EruditPhrases = {
63
63
  external_link_warn: 'You are going to visit external resource!',
64
64
  internal_link: 'Internal link',
65
65
  internal_link_warn: 'You are going to visit internal site page!',
66
- book: 'Book',
66
+ book: 'Textbook',
67
67
  group: 'Group',
68
68
  topic: 'Topic',
69
+ topics: 'Topics',
69
70
  article: 'Article',
70
71
  summary: 'Summary',
71
72
  practice: 'Practice',
@@ -93,6 +94,11 @@ const english: EruditPhrases = {
93
94
  sponsors_description:
94
95
  '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
96
  become_sponsor: 'Become a sponsor',
97
+ toc: 'Table of contents',
98
+ mentions: (count) => m(count, 'mention', 'mentions'),
99
+ start_learning: 'Start learning!',
100
+ x_contributors: (count) => m(count, 'Contributor', 'Contributors'),
101
+ x_sponsors: (count) => m(count, 'Sponsor', 'Sponsors'),
96
102
  };
97
103
 
98
104
  export default english;
package/languages/ru.ts CHANGED
@@ -46,7 +46,7 @@ const russian: EruditPhrases = {
46
46
  'Этот материал в разработке! Он может измениться в будущем и даже содержать ошибки!',
47
47
  flag_advanced: 'Профиль',
48
48
  flag_advanced_description:
49
- 'Этот материал предназначен для учеников с высоким уровнем понимания предмета. Информация здесь не предназначена для новичокв!',
49
+ 'Этот материал предназначен для учеников с высоким уровнем понимания предмета. Информация здесь не предназначена для новичков!',
50
50
  flag_secondary: 'Дополнение',
51
51
  flag_secondary_description:
52
52
  'Это дополнительный материал для тех, кто хочет глубже погрузиться предмет и получить дополнительные знания и контекст.',
@@ -63,9 +63,10 @@ const russian: EruditPhrases = {
63
63
  external_link_warn: 'Вы собираетесь перейти на внешний ресурс!',
64
64
  internal_link: 'Внутренняя ссылка',
65
65
  internal_link_warn: 'Вы собираетесь перейти на внутреннюю страницу сайта!',
66
- book: 'Книга',
66
+ book: 'Учебник',
67
67
  group: 'Группа',
68
68
  topic: 'Тема',
69
+ topics: 'Темы',
69
70
  article: 'Статья',
70
71
  summary: 'Конспект',
71
72
  practice: 'Задачи',
@@ -94,6 +95,12 @@ const russian: EruditPhrases = {
94
95
  sponsors_description:
95
96
  'Список людей и организаций, которые поддерживают проект финансово. Благодаря им проект может существовать и развиваться. Если вы хотите помочь проекту, то тоже можете стать спонсором и попасть на эту страницу!',
96
97
  become_sponsor: 'Стать спонсором',
98
+ toc: 'Содержание',
99
+ mentions: (count: number) =>
100
+ m(count, 'упоминание', 'упоминания', 'упоминаний'),
101
+ start_learning: 'Начать изучение!',
102
+ x_contributors: (count: number) => m(count, 'Автор', 'Автора', 'Авторов'),
103
+ x_sponsors: (count: number) => m(count, 'Спонсор', 'Спонсора', 'Спонсоров'),
97
104
  };
98
105
 
99
106
  export default russian;
@@ -107,6 +114,6 @@ export function m(
107
114
  ) {
108
115
  const text = [five, one, two, two, two, five][
109
116
  number % 100 > 10 && number % 100 < 20 ? 0 : Math.min(number % 10, 5)
110
- ];
117
+ ]!;
111
118
  return includeNumber ? `${number} ${text}` : text;
112
119
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "erudit",
3
- "version": "3.0.0-dev.21",
3
+ "version": "3.0.0-dev.23",
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.21",
19
- "@erudit-js/cli": "3.0.0-dev.21",
20
- "@erudit-js/bitran-elements": "3.0.0-dev.21"
18
+ "@erudit-js/cog": "3.0.0-dev.23",
19
+ "@erudit-js/cli": "3.0.0-dev.23",
20
+ "@erudit-js/bitran-elements": "3.0.0-dev.23"
21
21
  },
22
22
  "dependencies": {
23
23
  "@bitran-js/core": "1.0.0-dev.13",
@@ -1,16 +1,23 @@
1
+ import type { ContentGeneric } from '@shared/content/data/base';
2
+ import type { ContentTopicData } from '@shared/content/data/type/topic';
3
+ import type { ContentGroupData } from '@shared/content/data/type/group';
4
+ import type { ContentBookData } from '@shared/content/data/type/book';
5
+ import type { ContentGroupLike } from '@shared/content/data/groupLike';
6
+
1
7
  import { getTopicPartsLinks } from '@server/repository/topic';
2
8
  import { getContentBookFor } from '@server/repository/book';
3
9
  import { getContentGenericData } from '@server/repository/content';
4
10
  import { getFullContentId } from '@server/repository/contentId';
5
-
6
- import type { ContentGenericData } from '@shared/content/data/base';
7
- import type { ContentTopicData } from '@shared/content/data/type/topic';
8
- import type { ContentGroupData } from '@shared/content/data/type/group';
9
- import type { ContentBookData } from '@shared/content/data/type/book';
11
+ import { getQuickLinks } from '@server/repository/quickLink';
12
+ import { getContentToc } from '@server/repository/contentToc';
13
+ import { getContentSourceUsageSet } from '@server/repository/reference';
14
+ import { getReadLink } from '@server/repository/readLink';
15
+ import { getElementStats } from '@server/repository/elementStats';
16
+ import { countTopicsIn } from '@server/repository/topicCount';
10
17
 
11
18
  export default defineEventHandler(async (event) => {
12
19
  let contentId = getQuery(event)?.contentId as string;
13
- contentId = await getFullContentId(contentId);
20
+ contentId = getFullContentId(contentId);
14
21
 
15
22
  if (!contentId) {
16
23
  throw createError({
@@ -41,7 +48,7 @@ export default defineEventHandler(async (event) => {
41
48
  //
42
49
 
43
50
  async function getTopicData(
44
- generic: ContentGenericData,
51
+ generic: ContentGeneric,
45
52
  ): Promise<ContentTopicData> {
46
53
  const contentBook = await getContentBookFor(generic.contentId);
47
54
 
@@ -50,26 +57,41 @@ async function getTopicData(
50
57
  generic,
51
58
  bookTitle: contentBook?.title,
52
59
  topicPartLinks: await getTopicPartsLinks(generic.contentId),
60
+ quickLinks: await getQuickLinks(generic.contentId),
53
61
  };
54
62
  }
55
63
 
56
64
  async function getGroupData(
57
- generic: ContentGenericData,
65
+ generic: ContentGeneric,
58
66
  ): Promise<ContentGroupData> {
59
67
  const contentBook = await getContentBookFor(generic.contentId);
60
68
 
61
69
  return {
62
70
  type: 'group',
63
71
  generic,
72
+ groupLike: await getGroupLikeData(generic.contentId),
64
73
  bookTitle: contentBook?.title,
65
74
  };
66
75
  }
67
76
 
68
- async function getBookData(
69
- generic: ContentGenericData,
70
- ): Promise<ContentBookData> {
77
+ async function getBookData(generic: ContentGeneric): Promise<ContentBookData> {
71
78
  return {
72
79
  type: 'book',
73
80
  generic,
81
+ groupLike: await getGroupLikeData(generic.contentId),
82
+ };
83
+ }
84
+
85
+ //
86
+ //
87
+ //
88
+
89
+ async function getGroupLikeData(contentId: string): Promise<ContentGroupLike> {
90
+ return {
91
+ contentToc: await getContentToc(contentId),
92
+ readLink: await getReadLink(contentId),
93
+ topicCount: await countTopicsIn(contentId),
94
+ elementStats: await getElementStats(contentId),
95
+ sourceUsageSet: await getContentSourceUsageSet(contentId),
74
96
  };
75
97
  }
@@ -0,0 +1,46 @@
1
+ import type { IndexData } from '@shared/indexData';
2
+
3
+ import { getAllElementStats } from '@server/repository/elementStats';
4
+ import { countAllTopics } from '@server/repository/topicCount';
5
+ import { getFullContentToc } from '@server/repository/contentToc';
6
+ import { ERUDIT_SERVER } from '@server/global';
7
+ import { DbContributor } from '@server/db/entities/Contributor';
8
+ import { getSponsorIds, readSponsorConfig } from '@server/sponsor/repository';
9
+
10
+ export default defineEventHandler<Promise<IndexData>>(async () => {
11
+ return {
12
+ elementStats: await getAllElementStats(),
13
+ topicCount: await countAllTopics(),
14
+ contentToc: await getFullContentToc(),
15
+ contributors: await getIndexContributors(),
16
+ sponsors: await getIndexSponsors(),
17
+ };
18
+ });
19
+
20
+ async function getIndexContributors(): Promise<[string, string | undefined][]> {
21
+ const dbContributors = await ERUDIT_SERVER.DB.manager.find(DbContributor, {
22
+ select: ['contributorId', 'displayName', 'avatar'],
23
+ });
24
+
25
+ return dbContributors.map((contributor) => [
26
+ contributor.displayName || contributor.contributorId,
27
+ contributor.avatar ? '/contributors/' + contributor.avatar : undefined,
28
+ ]);
29
+ }
30
+
31
+ async function getIndexSponsors(): Promise<[string, string | undefined][]> {
32
+ const sponsorIds = getSponsorIds();
33
+ const indexSponsors: [string, string | undefined][] = [];
34
+
35
+ for (const sponsorId of sponsorIds) {
36
+ const config = await readSponsorConfig(sponsorId);
37
+ if (config) {
38
+ indexSponsors.push([
39
+ config.name || sponsorId,
40
+ ERUDIT_SERVER.SPONSORS?.avatars[sponsorId],
41
+ ]);
42
+ }
43
+ }
44
+
45
+ return indexSponsors;
46
+ }
@@ -95,20 +95,6 @@ async function retrieveContentFrom(location: BitranLocation) {
95
95
  };
96
96
  }
97
97
 
98
- if (location.type === 'group') {
99
- const dbGroup = await ERUDIT_SERVER.DB.manager.findOne(DbGroup, {
100
- select: ['contentId', 'content'],
101
- where: { contentId: location.path },
102
- });
103
-
104
- if (!dbGroup) throwNotFound();
105
-
106
- return {
107
- biCode: dbGroup!.content!,
108
- context: { location, aliases: NO_ALIASES() },
109
- };
110
- }
111
-
112
98
  if (location.type === 'contributor') {
113
99
  const dbContributor = await ERUDIT_SERVER.DB.manager.findOne(
114
100
  DbContributor,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  decodeBitranLocation,
3
+ isContentType,
3
4
  parseBitranLocation,
4
5
  } from '@erudit-js/cog/schema';
5
6
 
@@ -19,12 +20,8 @@ export async function parseClientBitranLocation(clientLocation: string) {
19
20
  try {
20
21
  const location = parseBitranLocation(clientLocation);
21
22
 
22
- switch (location.type) {
23
- case 'article':
24
- case 'summary':
25
- case 'practice':
26
- case 'group':
27
- location.path = getFullContentId(location.path!);
23
+ if (isContentType(location.type)) {
24
+ location.path = getFullContentId(location.path!);
28
25
  }
29
26
 
30
27
  return location;
@@ -11,13 +11,25 @@ import {
11
11
  import { AliasesNode } from '@erudit-js/bitran-elements/aliases/shared';
12
12
  import { DetailsNode } from '@erudit-js/bitran-elements/details/shared';
13
13
  import { HeadingNode } from '@erudit-js/bitran-elements/heading/shared';
14
+ import {
15
+ problemName,
16
+ ProblemNode,
17
+ problemsName,
18
+ ProblemsNode,
19
+ type ProblemParseData,
20
+ type ProblemsParseData,
21
+ } from '@erudit-js/bitran-elements/problem/shared';
14
22
 
15
23
  import { createBitranTranspiler } from '@server/bitran/transpiler';
16
24
  import { ERUDIT_SERVER } from '@server/global';
17
25
  import { DbUnique } from '@server/db/entities/Unique';
26
+ import { DbQuickLink } from '@erudit/server/plugin/db/entities/QuickLink';
27
+ import { logger } from '@server/logger';
28
+ import { DbStat } from '@erudit/server/plugin/db/entities/Stat';
18
29
 
19
30
  let context: BitranContext = {} as any;
20
31
  let bitranTranspiler: BitranTranspiler;
32
+ let stats: Record<string, number> = {};
21
33
 
22
34
  const blocksAfterHeading = 2;
23
35
 
@@ -25,9 +37,10 @@ export async function parseBitranContent(
25
37
  location: BitranLocation,
26
38
  biCode: string,
27
39
  ) {
28
- // Reset Bitran context in order not to create core each time
40
+ // Reset
29
41
  context.location = location;
30
42
  context.aliases = NO_ALIASES();
43
+ stats = {};
31
44
 
32
45
  bitranTranspiler ||= await createBitranTranspiler({
33
46
  context,
@@ -43,6 +56,9 @@ export async function parseBitranContent(
43
56
  const meta = node.meta;
44
57
  const uniqueId = meta?.id;
45
58
 
59
+ tryUpdateStats(node.name);
60
+ await tryAddQuickLink(node);
61
+
46
62
  if (node instanceof AliasesNode) {
47
63
  mergeAliases(context.aliases, node.parseData);
48
64
  }
@@ -68,6 +84,8 @@ export async function parseBitranContent(
68
84
  },
69
85
  });
70
86
 
87
+ await saveStats();
88
+
71
89
  for (const heading of headings) {
72
90
  let blocksAfter = 0;
73
91
  let content = await bitranTranspiler.stringifier.stringify(heading);
@@ -104,10 +122,86 @@ export async function parseBitranContent(
104
122
  dbUnique.location = createUniqueLocation(node);
105
123
  dbUnique.content =
106
124
  content || (await bitranTranspiler.stringifier.stringify(node));
125
+
107
126
  dbUnique.title = node.parseData?.title || node.meta?.title || null;
127
+
128
+ if (node instanceof ProblemNode || node instanceof ProblemsNode) {
129
+ dbUnique.title = node.parseData.info.title;
130
+ }
131
+
108
132
  dbUnique.productName = node.name;
109
133
  dbUnique.context = context;
110
134
 
111
135
  await ERUDIT_SERVER.DB.manager.save(dbUnique);
112
136
  }
113
137
  }
138
+
139
+ function tryUpdateStats(elementName: string) {
140
+ const trackedNames = (ERUDIT_SERVER.CONFIG?.bitran?.stat ?? []).flat();
141
+ if (trackedNames.includes(elementName)) {
142
+ stats[elementName] = (stats[elementName] || 0) + 1;
143
+ }
144
+ }
145
+
146
+ async function saveStats() {
147
+ for (const [elementName, count] of Object.entries(stats)) {
148
+ const dbStat = new DbStat();
149
+ dbStat.contentId = context.location.path!;
150
+ dbStat.elementName = elementName;
151
+ dbStat.count = count;
152
+ await ERUDIT_SERVER.DB.manager.save(dbStat);
153
+ }
154
+ }
155
+
156
+ async function tryAddQuickLink(element: ElementNode) {
157
+ const linkProperty = element.meta?.link;
158
+ if (linkProperty === undefined) return;
159
+
160
+ let linkLabel: string | undefined;
161
+
162
+ const linkCandidates = [
163
+ linkProperty,
164
+ element.parseData?.title,
165
+ element.meta?.title,
166
+ ];
167
+
168
+ if (element.name === problemName || element.name === problemsName) {
169
+ const problemParseData = element.parseData as
170
+ | ProblemParseData
171
+ | ProblemsParseData;
172
+ linkCandidates.unshift(problemParseData.info.title);
173
+ }
174
+
175
+ for (const linkCandidate of linkCandidates) {
176
+ if (typeof linkCandidate === 'string') {
177
+ const _label = linkCandidate.trim();
178
+ if (_label) {
179
+ linkLabel = _label;
180
+ break;
181
+ }
182
+ }
183
+ }
184
+
185
+ if (!linkLabel) {
186
+ logger.warn(
187
+ `Missing quick link label for element "${element.id}" at "${context.location.path!}"!`,
188
+ );
189
+ return;
190
+ }
191
+
192
+ const dbQuickLink = new DbQuickLink();
193
+ dbQuickLink.label = linkLabel;
194
+ dbQuickLink.elementName = element.name;
195
+ dbQuickLink.elementId = element.id;
196
+ dbQuickLink.contentId = context.location.path!;
197
+ dbQuickLink.contentType = context.location.type;
198
+
199
+ try {
200
+ await ERUDIT_SERVER.DB.manager.insert(DbQuickLink, dbQuickLink);
201
+ } catch (error) {
202
+ logger.warn(
203
+ `Failed to insert quick link "${linkLabel}" for element "${element.id}" at "${context.location.path!}"!`,
204
+ 'This is probably because the is already a quick link with same label!',
205
+ );
206
+ }
207
+ }