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.
- package/app/assets/icons/cameo-add.svg +3 -3
- package/app/assets/icons/diamond.svg +2 -2
- package/app/assets/icons/files.svg +5 -0
- package/app/assets/icons/list-squared.svg +3 -0
- package/app/assets/icons/rocket.svg +1 -0
- package/app/components/GroupLikePage.vue +70 -0
- package/app/components/QuickLinks.vue +99 -0
- package/app/components/aside/major/SiteInfo.vue +2 -2
- package/app/components/aside/major/panes/nav/NavBook.vue +1 -1
- package/app/components/aside/major/panes/other/ItemTheme.vue +25 -30
- package/app/components/index/IndexAvatars.vue +143 -0
- package/app/components/main/MainActionButton.vue +3 -1
- package/app/components/main/MainBitranContent.vue +13 -0
- package/app/components/main/MainDescription.vue +6 -0
- package/app/components/main/MainSectionTitle.vue +50 -0
- package/app/components/main/MainSourceUsages.vue +119 -0
- package/app/components/main/MainSourcesUsage.vue +60 -0
- package/app/components/main/MainToc.vue +135 -0
- package/app/components/main/content/ContentPopovers.vue +9 -2
- package/app/components/main/content/ContentReferences.vue +1 -27
- package/app/components/main/content/reference/ReferenceGroup.vue +10 -9
- package/app/components/main/content/reference/ReferenceSource.vue +1 -0
- package/app/components/main/topic/MainTopic.vue +5 -8
- package/app/components/main/topic/MainTopicQuickLinks.vue +24 -0
- package/app/components/stats/Stats.vue +21 -0
- package/app/components/stats/StatsGroupLike.vue +24 -0
- package/app/components/stats/StatsItem.vue +33 -0
- package/app/components/stats/StatsItemElement.vue +13 -0
- package/app/composables/bitranLocation.ts +2 -3
- package/app/composables/contentPage.ts +7 -6
- package/app/composables/theme.ts +26 -5
- package/app/pages/book/[...bookId].vue +6 -31
- package/app/pages/group/[...groupId].vue +5 -41
- package/app/pages/index.vue +189 -16
- package/app/pages/sponsors.vue +2 -2
- package/app/scripts/preview/data/pageLink.ts +4 -3
- package/app/scripts/preview/data/unique.ts +6 -6
- package/languages/en.ts +7 -1
- package/languages/ru.ts +10 -3
- package/package.json +4 -4
- package/server/api/content/data.ts +33 -11
- package/server/api/index/data.ts +46 -0
- package/server/plugin/bitran/content.ts +0 -14
- package/server/plugin/bitran/location.ts +3 -6
- package/server/plugin/build/jobs/content/parse.ts +95 -1
- package/server/plugin/build/jobs/content/type/group.ts +0 -21
- package/server/plugin/content/context.ts +3 -6
- package/server/plugin/db/entities/Group.ts +0 -3
- package/server/plugin/db/entities/QuickLink.ts +19 -0
- package/server/plugin/db/entities/Stat.ts +13 -0
- package/server/plugin/db/setup.ts +4 -0
- package/server/plugin/repository/content.ts +3 -3
- package/server/plugin/repository/contentToc.ts +90 -0
- package/server/plugin/repository/elementStats.ts +80 -0
- package/server/plugin/repository/link.ts +20 -0
- package/server/plugin/repository/quickLink.ts +36 -0
- package/server/plugin/repository/readLink.ts +17 -0
- package/server/plugin/repository/reference.ts +78 -0
- package/server/plugin/repository/topicCount.ts +19 -0
- package/shared/content/data/base.ts +2 -2
- package/shared/content/data/groupLike.ts +11 -0
- package/shared/content/data/type/book.ts +3 -1
- package/shared/content/data/type/group.ts +3 -1
- package/shared/content/data/type/topic.ts +3 -1
- package/shared/content/reference.ts +18 -0
- package/shared/content/toc.ts +35 -0
- package/shared/indexData.ts +10 -0
- package/shared/link.ts +3 -2
- package/shared/quickLink.ts +7 -0
- package/shared/stat.ts +23 -0
- package/shared/types/language.ts +6 -1
- package/utils/normalize.ts +7 -0
package/app/pages/index.vue
CHANGED
|
@@ -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
|
|
6
|
+
const pretty = useFormatText();
|
|
5
7
|
|
|
6
|
-
const
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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>
|
package/app/pages/sponsors.vue
CHANGED
|
@@ -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.
|
|
26
|
+
v-if="eruditConfig.sponsors?.addLink"
|
|
27
27
|
icon="diamond"
|
|
28
28
|
:label="phrase.become_sponsor"
|
|
29
|
-
:link="eruditConfig.
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
27
|
+
const route = trailingSlash(
|
|
30
28
|
`/api/preview/unique/${encodeBitranLocation(linkTarget._absoluteStrLocation!)}`,
|
|
31
|
-
|
|
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: '
|
|
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.
|
|
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.
|
|
19
|
-
"@erudit-js/cli": "3.0.0-dev.
|
|
20
|
-
"@erudit-js/bitran-elements": "3.0.0-dev.
|
|
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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
+
}
|