@eox/pages-theme-eox 0.11.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +30 -0
- package/.release-it.json +15 -0
- package/CHANGELOG.md +19 -0
- package/README.md +274 -0
- package/cypress/support/commands.js +25 -0
- package/cypress/support/component-index.html +11 -0
- package/cypress/support/component.js +22 -0
- package/cypress/support/mocks/features.data.js +9 -0
- package/cypress/support/mocks/helpers.js +63 -0
- package/cypress/support/mocks/vitepress.js +83 -0
- package/cypress.config.js +46 -0
- package/package.json +12 -2
- package/src/components/CTASection.cy.js +72 -0
- package/src/components/CookieBanner.cy.js +49 -0
- package/src/components/CookieBanner.vue +19 -6
- package/src/components/CookieSettings.cy.js +73 -0
- package/src/components/CookieSettings.vue +35 -27
- package/src/components/DataTable.cy.js +58 -0
- package/src/components/DataTable.vue +5 -1
- package/src/components/FeatureCard.cy.js +83 -0
- package/src/components/FeatureCard.vue +11 -5
- package/src/components/FeatureSection.cy.js +63 -0
- package/src/components/FeatureSection.vue +8 -4
- package/src/components/FeaturesGallery.cy.js +45 -0
- package/src/components/FeaturesGallery.vue +6 -3
- package/src/components/Footer.cy.js +77 -0
- package/src/components/Footer.vue +35 -14
- package/src/components/HeroSection.cy.js +66 -0
- package/src/components/LogoSection.cy.js +53 -0
- package/src/components/MobileNavDropdown.cy.js +67 -0
- package/src/components/MobileNavDropdown.vue +48 -0
- package/src/components/NavBar.cy.js +143 -0
- package/src/components/NavBar.vue +144 -8
- package/src/components/NavDropdown.cy.js +71 -0
- package/src/components/NavDropdown.vue +62 -0
- package/src/components/NewsBanner.cy.js +23 -0
- package/src/components/NewsBanner.vue +0 -1
- package/src/components/NotFound.cy.js +16 -0
- package/src/components/NotFound.vue +8 -2
- package/src/components/PricingTable.cy.js +91 -0
- package/src/components/PricingTable.vue +24 -7
- package/src/components/Tutorial.cy.js +85 -0
- package/src/components/Tutorial.vue +162 -0
- package/src/helpers.js +78 -0
- package/src/index.js +3 -0
- package/src/style.css +121 -4
- package/src/vitepressConfig.mjs +141 -95
|
@@ -34,16 +34,48 @@
|
|
|
34
34
|
</nav>
|
|
35
35
|
<ul class="list border large-space">
|
|
36
36
|
<li v-for="item in theme.nav.filter((item) => !item.action)">
|
|
37
|
+
<details v-if="item.items">
|
|
38
|
+
<summary>
|
|
39
|
+
<span class="max">{{ t(item.text, theme.i18n) }}</span>
|
|
40
|
+
<i class="mdi mdi-chevron-down"></i>
|
|
41
|
+
</summary>
|
|
42
|
+
<ul class="list">
|
|
43
|
+
<MobileNavDropdown :items="item.items" />
|
|
44
|
+
</ul>
|
|
45
|
+
</details>
|
|
37
46
|
<a
|
|
47
|
+
v-else
|
|
38
48
|
data-ui="#mobile-menu"
|
|
39
49
|
:href="withBase(item.link)"
|
|
40
50
|
:target="item.target"
|
|
41
51
|
:rel="item.rel"
|
|
42
52
|
:class="item.action ? 'button large medium-elevate cta' : ''"
|
|
43
53
|
>
|
|
44
|
-
<span>{{ item.text }}</span>
|
|
54
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
45
55
|
</a>
|
|
46
56
|
</li>
|
|
57
|
+
<li v-if="langs && langs.length > 1">
|
|
58
|
+
<details>
|
|
59
|
+
<summary>
|
|
60
|
+
<i class="mdi mdi-translate small"></i>
|
|
61
|
+
<span class="max">{{
|
|
62
|
+
langs.find((l) => l.active)?.label
|
|
63
|
+
}}</span>
|
|
64
|
+
<i class="mdi mdi-chevron-down small"></i>
|
|
65
|
+
</summary>
|
|
66
|
+
<ul class="list">
|
|
67
|
+
<li
|
|
68
|
+
v-for="lang in langs.filter((l) => !l.active)"
|
|
69
|
+
:key="lang.link"
|
|
70
|
+
:class="{ active: lang.active }"
|
|
71
|
+
>
|
|
72
|
+
<a :href="withBase(lang.link)" data-ui="#mobile-menu">
|
|
73
|
+
<span>{{ lang.label }}</span>
|
|
74
|
+
</a>
|
|
75
|
+
</li>
|
|
76
|
+
</ul>
|
|
77
|
+
</details>
|
|
78
|
+
</li>
|
|
47
79
|
</ul>
|
|
48
80
|
<div class="grid">
|
|
49
81
|
<div class="s12">
|
|
@@ -59,7 +91,7 @@
|
|
|
59
91
|
: 'primary-text'
|
|
60
92
|
"
|
|
61
93
|
>
|
|
62
|
-
<span>{{ item.text }}</span>
|
|
94
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
63
95
|
<i
|
|
64
96
|
v-if="
|
|
65
97
|
item.action === 'primary' || item.action === 'secondary'
|
|
@@ -70,6 +102,20 @@
|
|
|
70
102
|
</nav>
|
|
71
103
|
</div>
|
|
72
104
|
</div>
|
|
105
|
+
<div class="row center-align" v-if="theme.socialLinks">
|
|
106
|
+
<ul class="no-margin social-links">
|
|
107
|
+
<li v-for="link in theme.socialLinks">
|
|
108
|
+
<a
|
|
109
|
+
:href="link.link"
|
|
110
|
+
target="_blank"
|
|
111
|
+
rel="noopener"
|
|
112
|
+
class="button circle transparent"
|
|
113
|
+
>
|
|
114
|
+
<i :class="`mdi mdi-${link.icon}`"></i>
|
|
115
|
+
</a>
|
|
116
|
+
</li>
|
|
117
|
+
</ul>
|
|
118
|
+
</div>
|
|
73
119
|
</dialog>
|
|
74
120
|
</nav>
|
|
75
121
|
<nav class="large-padding no-margin center-align row holder nav-desktop">
|
|
@@ -90,12 +136,25 @@
|
|
|
90
136
|
<nav>
|
|
91
137
|
<ul class="left-align no-margin">
|
|
92
138
|
<li v-for="item in theme.nav.filter((i) => !i.action)">
|
|
139
|
+
<button
|
|
140
|
+
v-if="item.items"
|
|
141
|
+
class="button text"
|
|
142
|
+
:class="{ active: isActive(item, route.path) }"
|
|
143
|
+
>
|
|
144
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
145
|
+
<i class="mdi mdi-chevron-down"></i>
|
|
146
|
+
<menu class="no-wrap surface-container-lowest">
|
|
147
|
+
<NavDropdown :items="item.items" />
|
|
148
|
+
</menu>
|
|
149
|
+
</button>
|
|
93
150
|
<a
|
|
151
|
+
v-else
|
|
94
152
|
class="button text"
|
|
153
|
+
:class="{ active: isActive(item, route.path) }"
|
|
95
154
|
:href="withBase(item.link)"
|
|
96
155
|
:target="item.target"
|
|
97
156
|
:rel="item.rel"
|
|
98
|
-
>{{ item.text }}</a
|
|
157
|
+
>{{ t(item.text, theme.i18n) }}</a
|
|
99
158
|
>
|
|
100
159
|
</li>
|
|
101
160
|
</ul>
|
|
@@ -116,7 +175,7 @@
|
|
|
116
175
|
:rel="item.rel"
|
|
117
176
|
@click="trackEvent(['CTA', 'Click', 'Nav', item.text])"
|
|
118
177
|
>
|
|
119
|
-
<span>{{ item.text }}</span>
|
|
178
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
120
179
|
<i
|
|
121
180
|
v-if="item.action === 'primary' || item.action === 'secondary'"
|
|
122
181
|
class="mdi mdi-arrow-right"
|
|
@@ -124,16 +183,90 @@
|
|
|
124
183
|
</a>
|
|
125
184
|
</li>
|
|
126
185
|
</ul>
|
|
186
|
+
<ul class="left-align no-margin social-links" v-if="theme.socialLinks">
|
|
187
|
+
<li v-for="link in theme.socialLinks">
|
|
188
|
+
<a
|
|
189
|
+
:href="link.link"
|
|
190
|
+
target="_blank"
|
|
191
|
+
rel="noopener"
|
|
192
|
+
class="button circle transparent"
|
|
193
|
+
>
|
|
194
|
+
<i :class="`mdi mdi-${link.icon}`"></i>
|
|
195
|
+
</a>
|
|
196
|
+
</li>
|
|
197
|
+
</ul>
|
|
198
|
+
<ul class="left-align no-margin" v-if="langs && langs.length > 1">
|
|
199
|
+
<li>
|
|
200
|
+
<button class="button text">
|
|
201
|
+
<i class="mdi mdi-translate small"></i>
|
|
202
|
+
<i class="mdi mdi-chevron-down small"></i>
|
|
203
|
+
<menu class="no-wrap surface-container-lowest">
|
|
204
|
+
<li
|
|
205
|
+
v-for="lang in langs"
|
|
206
|
+
:key="lang.link"
|
|
207
|
+
:class="{ active: lang.active }"
|
|
208
|
+
>
|
|
209
|
+
<a :href="withBase(lang.link)">
|
|
210
|
+
<span>{{ lang.label }}</span>
|
|
211
|
+
</a>
|
|
212
|
+
</li>
|
|
213
|
+
</menu>
|
|
214
|
+
</button>
|
|
215
|
+
</li>
|
|
216
|
+
</ul>
|
|
127
217
|
</nav>
|
|
128
218
|
</nav>
|
|
129
219
|
</div>
|
|
130
220
|
</template>
|
|
131
221
|
|
|
132
222
|
<script setup>
|
|
133
|
-
import {
|
|
134
|
-
import {
|
|
223
|
+
import { computed } from "vue";
|
|
224
|
+
import { useData, useRoute, withBase } from "vitepress";
|
|
225
|
+
import { t, trackEvent, isActive } from "../helpers";
|
|
226
|
+
import NavDropdown from "./NavDropdown.vue";
|
|
227
|
+
import MobileNavDropdown from "./MobileNavDropdown.vue";
|
|
228
|
+
|
|
229
|
+
const { site, theme, localeIndex } = useData();
|
|
230
|
+
const route = useRoute();
|
|
231
|
+
|
|
232
|
+
const langs = computed(() => {
|
|
233
|
+
const locales = site.value.locales;
|
|
234
|
+
if (!locales || Object.keys(locales).length <= 1) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
return Object.entries(locales).map(([id, locale]) => {
|
|
238
|
+
const currentPath = route.path;
|
|
239
|
+
const base = withBase("");
|
|
240
|
+
let pathRelativeToBase = currentPath;
|
|
241
|
+
if (base !== "/" && currentPath.startsWith(base)) {
|
|
242
|
+
pathRelativeToBase = currentPath.substring(base.length);
|
|
243
|
+
}
|
|
135
244
|
|
|
136
|
-
const
|
|
245
|
+
const currentLocaleId = localeIndex.value;
|
|
246
|
+
const currentLocalePrefix =
|
|
247
|
+
currentLocaleId === "root" ? "" : `/${currentLocaleId}`;
|
|
248
|
+
|
|
249
|
+
let pathWithoutLocale = pathRelativeToBase;
|
|
250
|
+
if (
|
|
251
|
+
currentLocalePrefix &&
|
|
252
|
+
pathRelativeToBase.startsWith(currentLocalePrefix)
|
|
253
|
+
) {
|
|
254
|
+
pathWithoutLocale = pathRelativeToBase.substring(
|
|
255
|
+
currentLocalePrefix.length,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const targetLocalePrefix = id === "root" ? "" : `/${id}`;
|
|
260
|
+
let link = `${targetLocalePrefix}${pathWithoutLocale}`.replace(/\/+/g, "/");
|
|
261
|
+
if (!link) link = "/";
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
label: locale.label,
|
|
265
|
+
link: link,
|
|
266
|
+
active: id === currentLocaleId,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
});
|
|
137
270
|
|
|
138
271
|
if (!import.meta.env.SSR) {
|
|
139
272
|
const scrollListener = () => {
|
|
@@ -151,7 +284,7 @@ if (!import.meta.env.SSR) {
|
|
|
151
284
|
|
|
152
285
|
<style>
|
|
153
286
|
.top-nav {
|
|
154
|
-
position:
|
|
287
|
+
position: fixed;
|
|
155
288
|
z-index: 100;
|
|
156
289
|
top: 0;
|
|
157
290
|
background: var(--surface);
|
|
@@ -237,4 +370,7 @@ nav.nav-desktop {
|
|
|
237
370
|
display: flex;
|
|
238
371
|
}
|
|
239
372
|
}
|
|
373
|
+
details summary i.mdi-translate {
|
|
374
|
+
transform: rotate(0deg) !important;
|
|
375
|
+
}
|
|
240
376
|
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import NavDropdown from "./NavDropdown.vue";
|
|
2
|
+
import { __setRouteMock } from "../../cypress/support/mocks/vitepress";
|
|
3
|
+
|
|
4
|
+
describe("<NavDropdown />", () => {
|
|
5
|
+
const items = [
|
|
6
|
+
{ text: "Home", link: "/" },
|
|
7
|
+
{ text: "About", link: "/about" },
|
|
8
|
+
{
|
|
9
|
+
text: "Products",
|
|
10
|
+
items: [
|
|
11
|
+
{ text: "Product A", link: "/products/a" },
|
|
12
|
+
{ text: "Product B", link: "/products/b" },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
it("renders list of items", () => {
|
|
18
|
+
cy.mount(NavDropdown, {
|
|
19
|
+
props: {
|
|
20
|
+
items,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
cy.contains("Home").should("exist");
|
|
25
|
+
cy.contains("About").should("exist");
|
|
26
|
+
cy.contains("Products").should("exist");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("renders nested dropdowns", () => {
|
|
30
|
+
cy.mount(NavDropdown, {
|
|
31
|
+
props: {
|
|
32
|
+
items,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
cy.contains("Product A").should("exist");
|
|
37
|
+
cy.contains("Product B").should("exist");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("marks active item", () => {
|
|
41
|
+
__setRouteMock({ path: "/about" });
|
|
42
|
+
|
|
43
|
+
cy.mount(NavDropdown, {
|
|
44
|
+
props: {
|
|
45
|
+
items,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
cy.contains("a", "About").should("have.class", "active");
|
|
50
|
+
cy.contains("a", "Home").should("not.have.class", "active");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("marks parent active if child is active", () => {
|
|
54
|
+
__setRouteMock({ path: "/products/a" });
|
|
55
|
+
|
|
56
|
+
cy.mount(NavDropdown, {
|
|
57
|
+
props: {
|
|
58
|
+
items,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// The text 'Products' isn't an anchor tag in the dropdown logic when it has items,
|
|
63
|
+
// it depends on how NavDropdown relies on recursion.
|
|
64
|
+
// In NavDropdown.vue:
|
|
65
|
+
// <li v-if="item.items && item.text"> ... <a ... :class="{ active: isActive(item, route.path) }">
|
|
66
|
+
|
|
67
|
+
// So 'Products' (parent) should be active
|
|
68
|
+
// We target the closest 'a' or element with text Products that has class active
|
|
69
|
+
cy.contains("span", "Products").parent("a").should("have.class", "active");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-for="item in items">
|
|
3
|
+
<li v-if="item.items && item.text">
|
|
4
|
+
<a
|
|
5
|
+
v-if="item.link"
|
|
6
|
+
:href="withBase(item.link)"
|
|
7
|
+
:target="item.target"
|
|
8
|
+
:rel="item.rel"
|
|
9
|
+
class="row max padding"
|
|
10
|
+
:class="{ active: isActive(item, route.path) }"
|
|
11
|
+
>
|
|
12
|
+
<span class="max">{{ t(item.text, theme.i18n) }}</span>
|
|
13
|
+
<i class="mdi mdi-chevron-right"></i>
|
|
14
|
+
</a>
|
|
15
|
+
<a
|
|
16
|
+
v-else
|
|
17
|
+
class="row max padding"
|
|
18
|
+
:class="{ active: isActive(item, route.path) }"
|
|
19
|
+
>
|
|
20
|
+
<span class="max">{{ t(item.text, theme.i18n) }}</span>
|
|
21
|
+
<i class="mdi mdi-chevron-right"></i>
|
|
22
|
+
</a>
|
|
23
|
+
<menu class="no-wrap surface-container-lowest">
|
|
24
|
+
<NavDropdown :items="item.items" />
|
|
25
|
+
</menu>
|
|
26
|
+
</li>
|
|
27
|
+
<NavDropdown v-else-if="item.items" :items="item.items" />
|
|
28
|
+
<li v-else>
|
|
29
|
+
<a
|
|
30
|
+
v-if="item.link"
|
|
31
|
+
:href="withBase(item.link)"
|
|
32
|
+
:target="item.target"
|
|
33
|
+
:rel="item.rel"
|
|
34
|
+
class="row"
|
|
35
|
+
:class="{ active: isActive(item, route.path) }"
|
|
36
|
+
>
|
|
37
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
38
|
+
</a>
|
|
39
|
+
<span
|
|
40
|
+
v-else
|
|
41
|
+
class="row"
|
|
42
|
+
:class="{ active: isActive(item, route.path) }"
|
|
43
|
+
>{{ t(item.text, theme.i18n) }}</span
|
|
44
|
+
>
|
|
45
|
+
</li>
|
|
46
|
+
</template>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup>
|
|
50
|
+
import { withBase, useRoute, useData } from "vitepress";
|
|
51
|
+
import { isActive, t } from "../helpers";
|
|
52
|
+
|
|
53
|
+
defineProps({
|
|
54
|
+
items: {
|
|
55
|
+
type: Array,
|
|
56
|
+
required: true,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const route = useRoute();
|
|
61
|
+
const { theme } = useData();
|
|
62
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import NewsBanner from "./NewsBanner.vue";
|
|
2
|
+
|
|
3
|
+
describe("<NewsBanner />", () => {
|
|
4
|
+
it("renders content passed via prop", () => {
|
|
5
|
+
const content = "<strong>Important News:</strong> Something happened!";
|
|
6
|
+
cy.mount(NewsBanner, {
|
|
7
|
+
props: { content },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
cy.get(".news-banner").should(
|
|
11
|
+
"contain.html",
|
|
12
|
+
"<strong>Important News:</strong>",
|
|
13
|
+
);
|
|
14
|
+
cy.contains("Something happened!").should("be.visible");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("applies correct classes", () => {
|
|
18
|
+
cy.mount(NewsBanner, {
|
|
19
|
+
props: { content: "Test" },
|
|
20
|
+
});
|
|
21
|
+
cy.get(".news-banner").should("have.class", "primary");
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import NotFound from "./NotFound.vue";
|
|
2
|
+
|
|
3
|
+
describe("<NotFound />", () => {
|
|
4
|
+
it("renders 404 title and message", () => {
|
|
5
|
+
cy.mount(NotFound);
|
|
6
|
+
cy.contains("h1", "404").should("be.visible");
|
|
7
|
+
cy.contains("p", "The page you requested was not found").should(
|
|
8
|
+
"be.visible",
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("renders back to home link", () => {
|
|
13
|
+
cy.mount(NotFound);
|
|
14
|
+
cy.get('a[href="/"]').should("contain.text", "Back to home");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="VPPage">
|
|
3
3
|
<h1>404</h1>
|
|
4
|
-
<p>The page you requested was not found
|
|
4
|
+
<p>{{ t("The page you requested was not found.", theme.i18n) }}</p>
|
|
5
5
|
<div class="small-space"></div>
|
|
6
6
|
<nav>
|
|
7
7
|
<a class="button responsive-mobile" href="/">
|
|
8
8
|
<i class="mdi mdi-arrow-left"></i>
|
|
9
|
-
<span>Back to home</span>
|
|
9
|
+
<span>{{ t("Back to home", theme.i18n) }}</span>
|
|
10
10
|
</a>
|
|
11
11
|
</nav>
|
|
12
12
|
<div class="large-space"></div>
|
|
13
13
|
</div>
|
|
14
14
|
</template>
|
|
15
|
+
|
|
16
|
+
<script setup>
|
|
17
|
+
import { useData } from "vitepress";
|
|
18
|
+
import { t } from "../helpers";
|
|
19
|
+
const { theme } = useData();
|
|
20
|
+
</script>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import PricingTable from "./PricingTable.vue";
|
|
2
|
+
|
|
3
|
+
// Stub ClientOnly to render children immediately
|
|
4
|
+
const ClientOnly = {
|
|
5
|
+
template: "<div><slot></slot></div>",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
describe("<PricingTable />", () => {
|
|
9
|
+
const plans = [
|
|
10
|
+
{
|
|
11
|
+
name: "Basic",
|
|
12
|
+
price: 10,
|
|
13
|
+
id: "basic",
|
|
14
|
+
details: { Storage: { text: "1GB" } },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "Pro",
|
|
18
|
+
price: 20,
|
|
19
|
+
id: "pro",
|
|
20
|
+
details: { Storage: { text: "10GB" } },
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const options = [
|
|
24
|
+
{
|
|
25
|
+
id: "opt1",
|
|
26
|
+
label: "Extra Support",
|
|
27
|
+
prices: { basic: 5, pro: 10 },
|
|
28
|
+
checked: false,
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const details = [
|
|
32
|
+
{
|
|
33
|
+
title: "Features",
|
|
34
|
+
plans: [
|
|
35
|
+
{ name: "Storage", details: { Storage: "1GB" } },
|
|
36
|
+
{ name: "Storage", details: { Storage: "10GB" } },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const config = {
|
|
42
|
+
title: "Pricing",
|
|
43
|
+
plans,
|
|
44
|
+
options,
|
|
45
|
+
details,
|
|
46
|
+
contactLink: "mailto:sales@example.com",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
it("renders the table with plans", () => {
|
|
50
|
+
cy.mount(PricingTable, {
|
|
51
|
+
props: { config },
|
|
52
|
+
global: {
|
|
53
|
+
components: { ClientOnly },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
cy.contains("Pricing").should("exist");
|
|
58
|
+
cy.contains("Basic").should("exist");
|
|
59
|
+
cy.contains("Pro").should("exist");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("renders options checkboxes", () => {
|
|
63
|
+
cy.mount(PricingTable, {
|
|
64
|
+
props: { config },
|
|
65
|
+
global: {
|
|
66
|
+
components: { ClientOnly },
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
cy.contains("+ Extra Support").should("exist");
|
|
71
|
+
cy.get("input#opt1").should("exist");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("renders details rows", () => {
|
|
75
|
+
cy.mount(PricingTable, {
|
|
76
|
+
props: { config },
|
|
77
|
+
global: {
|
|
78
|
+
components: { ClientOnly },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
cy.contains("Storage").should("exist");
|
|
83
|
+
cy.contains("1GB").should("exist");
|
|
84
|
+
cy.contains("10GB").should("exist");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Note: Testing actual calculation logic inside the component might require
|
|
88
|
+
// asserting on the DOM price elements if they were exposed/visible.
|
|
89
|
+
// The component seems to update prices internally but without clear price display in the simplified view
|
|
90
|
+
// we extracted, we check for presence mostly.
|
|
91
|
+
});
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
:href="addPlanConfig(contactLink, plan)"
|
|
49
49
|
class="button responsive bold margin-top-1 margin-bottom-2"
|
|
50
50
|
>
|
|
51
|
-
Contact us
|
|
51
|
+
{{ t("Contact us", theme.i18n) }}
|
|
52
52
|
</a>
|
|
53
53
|
</ClientOnly>
|
|
54
54
|
</div>
|
|
@@ -58,7 +58,9 @@
|
|
|
58
58
|
<!-- Main Plans Table -->
|
|
59
59
|
<div class="wrapper" :style="gridStyle">
|
|
60
60
|
<div class="cell orig-col-1 l top-margin">
|
|
61
|
-
<h6 v-if="!localDetails.length" class="bold small">
|
|
61
|
+
<h6 v-if="!localDetails.length" class="bold small">
|
|
62
|
+
{{ t("Plans:", theme.i18n) }}
|
|
63
|
+
</h6>
|
|
62
64
|
</div>
|
|
63
65
|
<div
|
|
64
66
|
v-for="(plan, index) in localPlans"
|
|
@@ -69,7 +71,9 @@
|
|
|
69
71
|
<h6 class="primary-text bold top-margin">{{ plan.name }}</h6>
|
|
70
72
|
</div>
|
|
71
73
|
|
|
72
|
-
<div class="cell orig-col-1 l">
|
|
74
|
+
<div class="cell orig-col-1 l">
|
|
75
|
+
{{ t("Price (per month):", theme.i18n) }}
|
|
76
|
+
</div>
|
|
73
77
|
<div
|
|
74
78
|
v-for="(plan, index) in localPlans"
|
|
75
79
|
:key="'price-' + index"
|
|
@@ -142,7 +146,7 @@
|
|
|
142
146
|
:class="`surface-container-low cell bottom-cell orig-col-${index + 2} center-align`"
|
|
143
147
|
>
|
|
144
148
|
<a v-if="plan.link" :href="plan.link"
|
|
145
|
-
>See all features
|
|
149
|
+
>{{ t("See all features", theme.i18n) }}
|
|
146
150
|
<svg
|
|
147
151
|
style="width: 16px; height: 16px"
|
|
148
152
|
xmlns="http://www.w3.org/2000/svg"
|
|
@@ -168,7 +172,7 @@
|
|
|
168
172
|
v-if="showSales"
|
|
169
173
|
:href="addPlanConfig(contactLink, plan)"
|
|
170
174
|
class="button responsive bold"
|
|
171
|
-
>Contact us</a
|
|
175
|
+
>{{ t("Contact us", theme.i18n) }}</a
|
|
172
176
|
>
|
|
173
177
|
</ClientOnly>
|
|
174
178
|
</div>
|
|
@@ -185,7 +189,7 @@
|
|
|
185
189
|
|
|
186
190
|
<div class="wrapper" :style="secondaryGridStyle">
|
|
187
191
|
<div class="cell cell orig-col-1 l">
|
|
188
|
-
<div class="">Additional price
|
|
192
|
+
<div class="">{{ t("Additional price:", theme.i18n) }}</div>
|
|
189
193
|
</div>
|
|
190
194
|
<div
|
|
191
195
|
v-for="(plan, detailPlanIndex) in detail.plans"
|
|
@@ -231,15 +235,28 @@
|
|
|
231
235
|
</template>
|
|
232
236
|
|
|
233
237
|
<p class="small grey-text m-4" v-if="showVAT">
|
|
234
|
-
*
|
|
238
|
+
*
|
|
239
|
+
{{
|
|
240
|
+
t(
|
|
241
|
+
"All prices are given excluding VAT. Prices are valid until",
|
|
242
|
+
theme.i18n,
|
|
243
|
+
)
|
|
244
|
+
}}
|
|
235
245
|
{{ showVAT }}.
|
|
236
246
|
</p>
|
|
237
247
|
</div>
|
|
238
248
|
</template>
|
|
239
249
|
|
|
240
250
|
<script>
|
|
251
|
+
import { t } from "../helpers.js";
|
|
252
|
+
import { useData } from "vitepress";
|
|
253
|
+
|
|
241
254
|
export default {
|
|
242
255
|
name: "PricingTable",
|
|
256
|
+
setup() {
|
|
257
|
+
const { theme } = useData();
|
|
258
|
+
return { theme, t };
|
|
259
|
+
},
|
|
243
260
|
props: {
|
|
244
261
|
config: {
|
|
245
262
|
type: Object,
|