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
@@ -3,18 +3,22 @@ import { type ContentContributor } from '@shared/contributor';
3
3
  import { createContributorLink } from '@shared/link';
4
4
 
5
5
  defineProps<ContentContributor>();
6
-
7
- const Link = defineNuxtLink({ prefetch: false });
8
6
  </script>
9
7
 
10
8
  <template>
11
- <Link
9
+ <EruditLink
10
+ :prefetch="false"
12
11
  :class="$style.contributor"
13
12
  :to="createContributorLink(contributorId)"
14
13
  >
15
- <ContributorAvatar :contributorId :avatar />
14
+ <Avatar
15
+ icon="user"
16
+ :src="avatar ? `/contributors/${avatar}` : undefined"
17
+ :class="$style.avatar"
18
+ :color="stringColor(contributorId)"
19
+ />
16
20
  <span>{{ displayName || contributorId }}</span>
17
- </Link>
21
+ </EruditLink>
18
22
  </template>
19
23
 
20
24
  <style lang="scss" module>
@@ -31,5 +35,9 @@ const Link = defineNuxtLink({ prefetch: false });
31
35
  &:hover {
32
36
  background: var(--bgAccent);
33
37
  }
38
+
39
+ .avatar {
40
+ --avatarSize: 40px;
41
+ }
34
42
  }
35
43
  </style>
@@ -0,0 +1,51 @@
1
+ <script setup lang="ts">
2
+ import type { MyIconName } from '#my-icons';
3
+
4
+ defineProps<{
5
+ icon: MyIconName;
6
+ label: string;
7
+ link: string;
8
+ }>();
9
+ </script>
10
+
11
+ <template>
12
+ <section :class="$style.actionButtonSection">
13
+ <EruditLink
14
+ :prefetch="false"
15
+ :to="link"
16
+ target="_blank"
17
+ :class="$style.actionButton"
18
+ >
19
+ <MyIcon :name="icon" :class="$style.actionButtonIcon" />
20
+ <span :class="$style.actionButtonLabel">{{ label }}</span>
21
+ </EruditLink>
22
+ </section>
23
+ </template>
24
+
25
+ <style lang="scss" module>
26
+ .actionButtonSection {
27
+ display: flex;
28
+ justify-content: center;
29
+ align-items: center;
30
+ padding: var(--_pMainY) var(--_pMainX);
31
+ }
32
+
33
+ .actionButton {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 10px;
37
+ color: white;
38
+ text-decoration: none;
39
+ font-weight: 600;
40
+ background: var(--brand);
41
+ border-radius: 5px;
42
+ padding: 12px 20px;
43
+ box-shadow: 0 0 14px 1px color-mix(in srgb, var(--brand), transparent 60%);
44
+
45
+ @include transition(background);
46
+
47
+ &:hover {
48
+ background: color-mix(in srgb, var(--brand), transparent 25%);
49
+ }
50
+ }
51
+ </style>
@@ -6,7 +6,6 @@ import {
6
6
  } from '@erudit-js/cog/schema';
7
7
 
8
8
  import type { RawBitranContent } from '@shared/bitran/content';
9
- import ContentSection from '@app/components/main/content/ContentSection.vue';
10
9
 
11
10
  type MainBitranPayload = RawBitranContent & {
12
11
  mainLocation: BitranLocation;
@@ -41,7 +40,16 @@ if (
41
40
  </script>
42
41
 
43
42
  <template>
44
- <ContentSection v-if="payloadValue.biCode">
43
+ <MainSection v-if="payloadValue.biCode" :class="$style.mainBitranContent">
44
+ <template v-if="$slots.header" v-slot:header>
45
+ <slot name="header"></slot>
46
+ </template>
45
47
  <BitranContent :rawContent="payloadValue" />
46
- </ContentSection>
48
+ </MainSection>
47
49
  </template>
50
+
51
+ <style lang="scss" module>
52
+ .mainBitranContent {
53
+ padding-top: var(--_pMainY);
54
+ }
55
+ </style>
@@ -7,14 +7,10 @@ defineProps<{ items: BreadcrumbItem[] }>();
7
7
  <template>
8
8
  <section v-if="items.length" :class="$style.breadcrumb">
9
9
  <template v-for="item in items">
10
- <NuxtLink
11
- :to="item.link"
12
- :prefetch="false"
13
- :class="$style.breadcrumbItem"
14
- >
10
+ <EruditLink :to="item.link" x :class="$style.breadcrumbItem">
15
11
  <MyIcon :name="item.icon" wrapper="span" />
16
12
  {{ item.title }}
17
- </NuxtLink>
13
+ </EruditLink>
18
14
  <MyIcon :class="$style.sep" name="angle-right" />
19
15
  </template>
20
16
  </section>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <section :class="$style.mainSection">
3
+ <div :class="$style.hr">
4
+ <div :class="$style.shade"></div>
5
+ <div v-if="$slots.header" :class="$style.header">
6
+ <slot name="header"></slot>
7
+ </div>
8
+ </div>
9
+ <div :class="$style.content">
10
+ <slot></slot>
11
+ </div>
12
+ </section>
13
+ </template>
14
+
15
+ <style lang="scss" module>
16
+ .mainSection {
17
+ .hr {
18
+ position: relative;
19
+ border-bottom: 2px solid var(--border);
20
+ margin: var(--_pMainY) 0;
21
+
22
+ .shade {
23
+ position: absolute;
24
+ z-index: 0;
25
+ bottom: 0;
26
+ left: 0;
27
+ height: 50px;
28
+ width: 100%;
29
+ background: linear-gradient(to bottom, transparent, var(--bgAside));
30
+ }
31
+
32
+ .header {
33
+ position: relative;
34
+ z-index: 1;
35
+ }
36
+ }
37
+
38
+ &:nth-child(even of .mainSection) {
39
+ .hr {
40
+ transform: scaleY(-1);
41
+
42
+ .header {
43
+ transform: scaleY(-1);
44
+ }
45
+ }
46
+ }
47
+
48
+ .content {
49
+ position: relative;
50
+ z-index: 1;
51
+ }
52
+ }
53
+
54
+ :not(.mainSection):has(+ .mainSection) {
55
+ position: relative;
56
+ z-index: 1;
57
+ }
58
+ </style>
@@ -0,0 +1,135 @@
1
+ <script lang="ts" setup>
2
+ import { type Cameo } from '@erudit-js/cog/schema';
3
+
4
+ const nuxtApp = useNuxtApp();
5
+ const currentCameo = shallowRef<Cameo>();
6
+
7
+ const payloadKey = 'cameo';
8
+ const payloadValue = shallowRef<{
9
+ cameos: Record<string, Cameo>;
10
+ sponsorCameos: Record<string, Cameo>;
11
+ }>();
12
+
13
+ async function init() {
14
+ payloadValue.value = nuxtApp.payload.data[payloadKey] ||= {
15
+ cameos: Object.fromEntries(
16
+ (await $fetch('/api/cameo/ids', { responseType: 'json' })).map(
17
+ (id: string) => [id, null],
18
+ ),
19
+ ),
20
+ sponsorCameos: Object.fromEntries(
21
+ (
22
+ await $fetch('/api/sponsor/cameo/ids', { responseType: 'json' })
23
+ ).map((id: string) => [id, null]),
24
+ ),
25
+ };
26
+
27
+ if (
28
+ Object.keys(payloadValue.value!.cameos).length === 0 &&
29
+ Object.keys(payloadValue.value!.sponsorCameos).length === 0
30
+ ) {
31
+ return;
32
+ }
33
+
34
+ await nextCameo();
35
+ }
36
+
37
+ function getAllAvailableCameos(): Array<{
38
+ id: string;
39
+ type: 'cameo' | 'sponsor';
40
+ }> {
41
+ const cameoIds = Object.keys(payloadValue.value!.cameos);
42
+ const sponsorIds = Object.keys(payloadValue.value!.sponsorCameos);
43
+
44
+ return [
45
+ ...cameoIds.map((id) => ({ id, type: 'cameo' as const })),
46
+ ...sponsorIds.map((id) => ({ id, type: 'sponsor' as const })),
47
+ ];
48
+ }
49
+
50
+ function getRandomCameoExcluding(
51
+ exclude?: string,
52
+ ): { id: string; type: 'cameo' | 'sponsor' } | null {
53
+ const availableCameos = getAllAvailableCameos();
54
+
55
+ if (availableCameos.length === 0) return null;
56
+
57
+ // Filter out the current cameo if we need to exclude it
58
+ const filteredCameos = exclude
59
+ ? availableCameos.filter((cameo) => cameo.id !== exclude)
60
+ : availableCameos;
61
+
62
+ // If we only have one cameo and it's the current one, return it anyway
63
+ const targetCameos =
64
+ filteredCameos.length > 0 ? filteredCameos : availableCameos;
65
+
66
+ const randomIndex = Math.floor(Math.random() * targetCameos.length);
67
+ return targetCameos[randomIndex]!;
68
+ }
69
+
70
+ function getNextCameoId(): { id: string; type: 'cameo' | 'sponsor' } | null {
71
+ const currentCameoId = currentCameo.value?.cameoId;
72
+ return getRandomCameoExcluding(currentCameoId);
73
+ }
74
+
75
+ async function getCameoData(cameoId: string, type: 'cameo' | 'sponsor') {
76
+ const targetPayload =
77
+ type === 'cameo'
78
+ ? payloadValue.value!.cameos
79
+ : payloadValue.value!.sponsorCameos;
80
+
81
+ if (targetPayload[cameoId]) {
82
+ return targetPayload[cameoId];
83
+ }
84
+
85
+ const apiPath =
86
+ type === 'cameo'
87
+ ? `/api/cameo/data/${cameoId}`
88
+ : `/api/sponsor/cameo/data/${cameoId}`;
89
+
90
+ const cameoData = (await $fetch(apiPath, {
91
+ responseType: 'json',
92
+ })) as Cameo;
93
+
94
+ targetPayload[cameoId] = cameoData;
95
+ return cameoData;
96
+ }
97
+
98
+ async function nextCameo() {
99
+ const nextCameoInfo = getNextCameoId();
100
+ if (!nextCameoInfo) return;
101
+
102
+ currentCameo.value = await getCameoData(
103
+ nextCameoInfo.id,
104
+ nextCameoInfo.type,
105
+ );
106
+ }
107
+
108
+ const hasMultipleCameos = computed(() => {
109
+ if (!payloadValue.value) return false;
110
+
111
+ const totalCameos =
112
+ Object.keys(payloadValue.value.cameos).length +
113
+ Object.keys(payloadValue.value.sponsorCameos).length;
114
+ return totalCameos > 1;
115
+ });
116
+
117
+ onMounted(init);
118
+ </script>
119
+
120
+ <template>
121
+ <section v-if="currentCameo" :class="$style.cameo">
122
+ <MainCameoData
123
+ :key="currentCameo.cameoId"
124
+ :data="currentCameo"
125
+ :hasMultipleCameos
126
+ v-on:next-cameo="nextCameo"
127
+ />
128
+ </section>
129
+ </template>
130
+
131
+ <style lang="scss" module>
132
+ .cameo {
133
+ padding: var(--_pMainY) var(--_pMainX);
134
+ }
135
+ </style>
@@ -0,0 +1,232 @@
1
+ <script lang="ts" setup>
2
+ import { type Cameo } from '@erudit-js/cog/schema';
3
+
4
+ import eruditConfig from '#erudit/config';
5
+ import { detectStrictFileType } from '@erudit/utils/ext';
6
+
7
+ const props = defineProps<{
8
+ data: Cameo;
9
+ hasMultipleCameos: boolean;
10
+ }>();
11
+ const emit = defineEmits<{
12
+ (e: 'nextCameo'): void;
13
+ }>();
14
+
15
+ function getRandomAvatar(data: Cameo) {
16
+ const avatars = data.avatars;
17
+
18
+ if (!avatars || avatars.length === 0) {
19
+ throw new Error(
20
+ `No avatars available for the cameo with ID: ${data.cameoId}`,
21
+ );
22
+ }
23
+
24
+ const url = avatars[Math.floor(Math.random() * avatars.length)]!;
25
+ let type = detectStrictFileType(url, 'image', 'video');
26
+
27
+ return { url, type };
28
+ }
29
+
30
+ const { url: avatarSrc, type: avatarType } = getRandomAvatar(props.data);
31
+
32
+ const message =
33
+ props.data.messages[
34
+ Math.floor(Math.random() * props.data.messages.length)
35
+ ]!;
36
+ </script>
37
+
38
+ <template>
39
+ <div
40
+ :class="$style.cameoData"
41
+ :style="{ '--cameoColor': data.color ?? stringColor(data.cameoId) }"
42
+ >
43
+ <a :href="data.link" :class="$style.avatar" target="_blank">
44
+ <Avatar
45
+ :src="avatarSrc"
46
+ :color="data.color ?? stringColor(data.cameoId)"
47
+ :styling="{ glow: true, border: true }"
48
+ />
49
+ </a>
50
+ <div :class="$style.info">
51
+ <div :class="$style.triangleBorder">
52
+ <div :class="$style.triangle"></div>
53
+ </div>
54
+ <div :class="$style.message" v-html="message"></div>
55
+ <footer>
56
+ <MyRuntimeIcon
57
+ v-if="data.icon"
58
+ name="cameo-icon"
59
+ :svg="data.icon"
60
+ :class="$style.icon"
61
+ />
62
+ <a :class="$style.name" :href="data.link" target="_blank">
63
+ {{ data.name }}
64
+ </a>
65
+ <MyIcon
66
+ v-if="data.link"
67
+ :class="$style.external"
68
+ name="link-external"
69
+ />
70
+ <span :style="{ flex: 1 }"></span>
71
+ <span :class="$style.actions">
72
+ <EruditLink v-if="eruditConfig.sponsors" to="/sponsors/">
73
+ <MyIcon name="cameo-add" />
74
+ </EruditLink>
75
+ <MyIcon
76
+ v-if="hasMultipleCameos"
77
+ name="arrow-left"
78
+ @click="emit('nextCameo')"
79
+ :style="{ transform: 'scale(-1)' }"
80
+ />
81
+ </span>
82
+ </footer>
83
+ </div>
84
+ </div>
85
+ </template>
86
+
87
+ <style lang="scss" module>
88
+ @use '$/def/bp';
89
+
90
+ .cameoData {
91
+ display: flex;
92
+ align-items: start;
93
+ gap: 40px;
94
+
95
+ @include bp.below('mobile') {
96
+ gap: 28px;
97
+ }
98
+
99
+ .avatar > * {
100
+ --avatarSize: 60px;
101
+
102
+ @include bp.below('mobile') {
103
+ --avatarSize: 40px;
104
+ }
105
+ }
106
+
107
+ .info {
108
+ --cameoTriangleSize: 24px;
109
+ --cameoBg: color-mix(
110
+ in srgb,
111
+ light-dark(#ffffff, #1c1c1e),
112
+ var(--cameoColor) 15%
113
+ );
114
+ --cameoBorder: color-mix(in srgb, var(--border), var(--cameoColor) 30%);
115
+
116
+ flex: 1;
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 10px;
120
+ position: relative;
121
+ background: var(--cameoBg);
122
+ border: 2px solid var(--cameoBorder);
123
+ border-radius: 5px;
124
+ border-top-left-radius: 0;
125
+ padding: var(--gap);
126
+ font-family: 'Noto Serif', serif;
127
+ color: color-mix(in srgb, var(--text), var(--cameoColor) 10%);
128
+
129
+ @include bp.below('mobile') {
130
+ --cameoTriangleSize: 16px;
131
+ padding: var(--gapSmall);
132
+ }
133
+
134
+ .message {
135
+ font-size: 1.05em;
136
+
137
+ strong {
138
+ font-weight: 500;
139
+ color: color-mix(
140
+ in srgb,
141
+ var(--textDeep),
142
+ var(--cameoColor) 20%
143
+ );
144
+ }
145
+ }
146
+
147
+ footer {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ font-size: 0.95em;
152
+ flex-wrap: wrap;
153
+
154
+ .icon {
155
+ color: color-mix(in srgb, var(--text), var(--cameoColor) 80%);
156
+ }
157
+
158
+ .name {
159
+ font-family: 'Noto Sans', sans-serif;
160
+ color: inherit;
161
+ text-decoration-color: transparent;
162
+ font-weight: 600;
163
+
164
+ @include transition(text-decoration-color);
165
+
166
+ &:hover {
167
+ text-decoration-color: inherit;
168
+ }
169
+ }
170
+
171
+ .external {
172
+ opacity: 0.35;
173
+ font-size: 0.7em;
174
+ margin-left: 3px;
175
+ }
176
+
177
+ .actions {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 8px;
181
+ font-size: 0.9em;
182
+
183
+ > * {
184
+ color: color-mix(
185
+ in srgb,
186
+ var(--textDeep),
187
+ var(--cameoColor) 40%
188
+ );
189
+ cursor: pointer;
190
+ opacity: 0.3;
191
+ padding: 5px;
192
+ position: relative;
193
+ top: 6px;
194
+
195
+ @include transition(opacity);
196
+
197
+ &:hover {
198
+ opacity: 1;
199
+ }
200
+
201
+ @include bp.below('mobile') {
202
+ top: 0;
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ .triangle,
209
+ .triangleBorder {
210
+ position: absolute;
211
+ border-style: solid;
212
+ }
213
+
214
+ .triangleBorder {
215
+ top: -2px;
216
+ left: calc(-1 * var(--cameoTriangleSize));
217
+ border-width: 0 var(--cameoTriangleSize) var(--cameoTriangleSize) 0;
218
+ border-color: transparent var(--cameoBorder) transparent transparent;
219
+ }
220
+
221
+ .triangle {
222
+ --_smallerTriangleSize: calc(var(--cameoTriangleSize) - 4px);
223
+
224
+ left: 5px;
225
+ top: 2px;
226
+ border-width: 0 var(--_smallerTriangleSize)
227
+ var(--_smallerTriangleSize) 0;
228
+ border-color: transparent var(--cameoBg) transparent transparent;
229
+ }
230
+ }
231
+ }
232
+ </style>
@@ -71,7 +71,7 @@ const hasPopovers = computed(() => {
71
71
  <ul :class="$style.dependenciesList">
72
72
  <li v-for="(value, key) in generic.dependencies">
73
73
  <MyIcon :name="'arrow-left'" wrapper="span" />
74
- <NuxtLink :prefetch="false" :to="key">{{ value }}</NuxtLink>
74
+ <EruditLink :to="key">{{ value }}</EruditLink>
75
75
  </li>
76
76
  </ul>
77
77
  </ContentPopover>
@@ -5,13 +5,11 @@ import eruditConfig from '#erudit/config';
5
5
 
6
6
  import { type ContentTopicData } from '@shared/content/data/type/topic';
7
7
  import { TOPIC_PART_ICON } from '@shared/icons';
8
- import { topicLocation } from '@app/scripts/aside/minor/topic';
9
8
 
10
9
  import ContentBreadcrumb from '@app/components/main/content/ContentBreadcrumb.vue';
11
10
  import ContentDecoration from '@app/components/main/content/ContentDecoration.vue';
12
11
  import ContentPopovers from '@app/components/main/content/ContentPopovers.vue';
13
12
  import ContentReferences from '@app/components/main/content/ContentReferences.vue';
14
- import ContentSection from '@app/components/main/content/ContentSection.vue';
15
13
  import TopicPartSwitch from './TopicPartSwitch.vue';
16
14
 
17
15
  const location = useBitranLocation() as Ref<BitranLocation>;
@@ -21,13 +19,6 @@ const topicData = await useContentData<ContentTopicData>();
21
19
  await useContentPage(topicData);
22
20
 
23
21
  const phrase = await usePhrases('article', 'summary', 'practice');
24
-
25
- onMounted(() => {
26
- watchEffect(() => {
27
- // Telling live toc that content is mounted
28
- topicLocation.value = location.value;
29
- });
30
- });
31
22
  </script>
32
23
 
33
24
  <template>
@@ -54,22 +45,26 @@ onMounted(() => {
54
45
 
55
46
  <ContentPopovers :generic="topicData.generic" />
56
47
 
57
- <TopicPartSwitch
58
- :partLinks="topicData.topicPartLinks"
59
- :active="topicPart"
60
- />
48
+ <MainCameo />
61
49
 
62
50
  <div style="clear: both"></div>
63
51
 
64
- <MainBitranContent :location />
52
+ <MainBitranContent :location>
53
+ <template v-slot:header>
54
+ <TopicPartSwitch
55
+ :partLinks="topicData.topicPartLinks"
56
+ :active="topicPart"
57
+ />
58
+ </template>
59
+ </MainBitranContent>
65
60
 
66
- <ContentSection v-if="topicData.generic.references">
61
+ <MainSection v-if="topicData.generic.references">
67
62
  <ContentReferences :references="topicData.generic.references" />
68
- </ContentSection>
63
+ </MainSection>
69
64
 
70
- <ContentSection v-if="adsAllowed() && eruditConfig.ads?.bottom">
65
+ <MainSection v-if="adsAllowed() && eruditConfig.ads?.bottom">
71
66
  <AdsBannerBottom />
72
- </ContentSection>
67
+ </MainSection>
73
68
  </template>
74
69
 
75
70
  <style lang="scss" module>
@@ -7,13 +7,13 @@ import { TOPIC_PART_ICON } from '@erudit/shared/icons';
7
7
  defineProps<{ partLinks: TopicPartLinks; active: TopicPart }>();
8
8
 
9
9
  const phrase = await usePhrases('article', 'summary', 'practice');
10
- const Link = defineNuxtLink({ prefetch: false });
11
10
  </script>
12
11
 
13
12
  <template>
14
13
  <section :class="$style.topicPartSwitch">
15
- <Link
14
+ <EruditLink
16
15
  v-for="topicPart of topicParts"
16
+ :prefetch="false"
17
17
  :to="partLinks[topicPart]"
18
18
  :class="[
19
19
  $style.partButton,
@@ -23,7 +23,7 @@ const Link = defineNuxtLink({ prefetch: false });
23
23
  >
24
24
  <MyIcon :name="TOPIC_PART_ICON[topicPart]" />
25
25
  <div :class="$style.label">{{ phrase[topicPart] }}</div>
26
- </Link>
26
+ </EruditLink>
27
27
  </section>
28
28
  </template>
29
29
 
@@ -33,23 +33,18 @@ const Link = defineNuxtLink({ prefetch: false });
33
33
  .topicPartSwitch {
34
34
  --height: 50px;
35
35
 
36
- position: relative;
37
- top: 65px;
38
-
39
36
  display: flex;
40
37
  align-items: end;
41
38
  justify-content: center;
42
39
  gap: var(--gapBig);
43
- margin: var(--_pMainY) 0;
44
- margin-top: -40px;
40
+ margin: var(--_pMainY) 0 0;
41
+
42
+ position: relative;
43
+ top: 2px;
45
44
 
46
45
  width: 100%;
47
46
  height: var(--height);
48
47
  border-bottom: 2px solid transparent;
49
-
50
- @include bp.below('mobile') {
51
- top: 56px;
52
- }
53
48
  }
54
49
 
55
50
  .partButton {
@@ -7,7 +7,7 @@ defineProps<{
7
7
  brand?: boolean;
8
8
  }>();
9
9
 
10
- const nuxtLink = defineNuxtLink({ prefetch: false });
10
+ const nuxtLink = defineNuxtLink({ prefetch: false, trailingSlash: 'append' });
11
11
  </script>
12
12
 
13
13
  <template>