@eox/pages-theme-eox 2.0.0-alpha.1 → 2.0.0-alpha.10
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 +4 -1
- package/README.md +75 -11
- package/cypress/support/mocks/authors.data.js +14 -0
- package/cypress/support/mocks/helpers.js +65 -4
- package/cypress.config.js +9 -0
- package/package.json +3 -1
- package/src/Layout.vue +14 -3
- package/src/components/BlogList.vue +74 -55
- package/src/components/BlogPost.vue +46 -24
- package/src/components/CTASection.vue +18 -4
- package/src/components/FeatureCard/Index.vue +113 -26
- package/src/components/FeatureCard/Poster.vue +25 -3
- package/src/components/FeatureSection.cy.js +16 -0
- package/src/components/FeatureSection.vue +3 -8
- package/src/components/FeaturesGallery.vue +34 -2
- package/src/components/Footer.cy.js +52 -2
- package/src/components/Footer.vue +17 -5
- package/src/components/HeroSection.cy.js +20 -0
- package/src/components/HeroSection.vue +139 -24
- package/src/components/ImageCarousel.vue +11 -2
- package/src/components/ImageElement.vue +16 -4
- package/src/components/ImageGallery.vue +29 -4
- package/src/components/LogoSection.vue +26 -3
- package/src/components/MobileNavDropdown.vue +13 -3
- package/src/components/NavBar.cy.js +15 -0
- package/src/components/NavBar.vue +149 -21
- package/src/components/NavDropdown.vue +12 -2
- package/src/components/OpenPositions.vue +292 -0
- package/src/components/TagList.vue +32 -14
- package/src/components/TeamPage.vue +34 -20
- package/src/components/TestimonialsSection.cy.js +2 -5
- package/src/components/TestimonialsSection.vue +119 -16
- package/src/components/TextSection.cy.js +115 -0
- package/src/components/TextSection.vue +396 -0
- package/src/helpers.js +182 -37
- package/src/helpers.test.js +181 -0
- package/src/index.js +4 -0
- package/src/pages/author/[author].md +1 -0
- package/src/pages/author/[author].paths.js +2 -2
- package/src/pages/blog.md +1 -0
- package/src/pages/tag/[tag].md +1 -0
- package/src/pages/tag.md +1 -0
- package/src/pages/team.md +2 -1
- package/src/posts.data.js +39 -1
- package/src/style.css +232 -11
- package/src/tagUtils.js +5 -5
- package/src/vitepressConfig.mjs +6 -3
package/.gitlab-ci.yml
CHANGED
|
@@ -10,9 +10,11 @@ cache:
|
|
|
10
10
|
- package-lock.json
|
|
11
11
|
paths:
|
|
12
12
|
- .npm/
|
|
13
|
+
- .cache/Cypress
|
|
13
14
|
|
|
14
15
|
variables:
|
|
15
16
|
npm_config_cache: "$CI_PROJECT_DIR/.npm"
|
|
17
|
+
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/.cache/Cypress"
|
|
16
18
|
|
|
17
19
|
security:
|
|
18
20
|
stage: security
|
|
@@ -33,6 +35,7 @@ test:
|
|
|
33
35
|
stage: test
|
|
34
36
|
script:
|
|
35
37
|
- npm ci
|
|
36
|
-
-
|
|
38
|
+
- npx cypress install
|
|
39
|
+
- npm run test
|
|
37
40
|
rules:
|
|
38
41
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
package/README.md
CHANGED
|
@@ -145,26 +145,40 @@ The theme provides several built-in components and layout features. Some are dri
|
|
|
145
145
|
|
|
146
146
|
### Hero Section
|
|
147
147
|
|
|
148
|
-
To add a Hero section to the top of a page, use the `hero` frontmatter object.
|
|
148
|
+
To add a Hero section to the top of a page, use the `hero` frontmatter object. Heros feature a dark gradient transitioning from **DARK (top) to LIGHT (bottom)** (`.hero-gradient-bg`), allowing the top navigation bar to seamlessly sit transparently over the hero. On the landing page, the hero is larger and incorporates an isometric line pattern.
|
|
149
149
|
|
|
150
150
|
```yaml
|
|
151
151
|
---
|
|
152
152
|
hero:
|
|
153
|
-
title: "Welcome to My Site"
|
|
154
|
-
tagline: "Building the future of
|
|
153
|
+
text: "Welcome to My Site" # or title: "Welcome to My Site"
|
|
154
|
+
tagline: "Building the future of geospatial engineering"
|
|
155
155
|
image:
|
|
156
156
|
src: "/hero-image.png"
|
|
157
157
|
alt: "Hero Image"
|
|
158
158
|
actions:
|
|
159
159
|
- text: "Get Started"
|
|
160
160
|
link: "/start"
|
|
161
|
-
|
|
161
|
+
theme: "brand" # "brand" (primary surface), "secondary", or "alt" (bordered)
|
|
162
162
|
- text: "Learn More"
|
|
163
163
|
link: "/about"
|
|
164
|
-
|
|
164
|
+
theme: "secondary"
|
|
165
165
|
---
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
#### Hero on Aggregator Pages (`blog`, `team`, `tags`)
|
|
169
|
+
|
|
170
|
+
Pages using aggregator layouts (`layout: team`, `layout: blog`, or `layout: tags`) support optional heroes:
|
|
171
|
+
|
|
172
|
+
- **Without Hero (when `hero` is omitted or `hero: false`)**: The page renders a standard in-page header title beneath the solid top navigation bar.
|
|
173
|
+
- **With Hero (`hero: true` or custom `hero: { text, tagline, ... }`)**: The page renders the full dark hero with the `#000C14` → `#004170` gradient spanning seamlessly behind the transparent navigation bar. Setting `hero: true` automatically supplies appropriate layout titles (including dynamic `Tag: <tag>` and `Author: <author>` for filtered blog listings). Custom text, taglines, images, and actions can also be provided via the `hero` frontmatter object.
|
|
174
|
+
|
|
175
|
+
#### Gradient Design Standards
|
|
176
|
+
|
|
177
|
+
The theme standardizes two distinct gradient directions for dark visual elements using `#000C14` and `#004170`:
|
|
178
|
+
|
|
179
|
+
- **Hero Gradient (`.hero-gradient-bg`)**: Always transitions from **DARK (top) to LIGHT (bottom)**, running from `#000C14` (top) to `#004170` (bottom). Used for the landing page hero and all sub-page heroes. This anchors the fixed header area with dark contrast under navigation links while flowing naturally into lighter content below.
|
|
180
|
+
- **Dark Sections Gradient (`.primary-gradient-bg` / `.dark-section-gradient-bg`)**: Used in intermediate dark page sections further down the page (such as `<CTASection dark>`, feature highlights, or dark showcase galleries). Always transitions from **LIGHT (top) to DARK (bottom)**, running from `#004170` (top) to `#000C14` (bottom).
|
|
181
|
+
|
|
168
182
|
### Feature Section
|
|
169
183
|
|
|
170
184
|
Use the `<FeatureSection>` component to highlight specific features.
|
|
@@ -223,7 +237,7 @@ A flexible pricing table component.
|
|
|
223
237
|
|
|
224
238
|
#### CTA Section
|
|
225
239
|
|
|
226
|
-
A Call-to-Action section typically placed at the bottom of a page to encourage user engagement.
|
|
240
|
+
A Call-to-Action section typically placed at the bottom of a page to encourage user engagement. By default, it renders with a clean white (`surface-container-lowest`) background, or a dark gradient when the `dark` prop is passed.
|
|
227
241
|
|
|
228
242
|
```html
|
|
229
243
|
<CTASection
|
|
@@ -334,7 +348,7 @@ A standard content card with an icon or image, title, optional chips, metadata,
|
|
|
334
348
|
section-title="All Features"
|
|
335
349
|
variant="default"
|
|
336
350
|
columns="3/2/1"
|
|
337
|
-
background="surface-container-
|
|
351
|
+
background="surface-container-lowest"
|
|
338
352
|
:cards="[
|
|
339
353
|
{
|
|
340
354
|
id: 1,
|
|
@@ -572,7 +586,14 @@ feedback: false
|
|
|
572
586
|
The theme exports a set of shared utility and helper functions from its helpers module. You can import and use these helpers in your custom VitePress components, checkout pages, or inline script setups:
|
|
573
587
|
|
|
574
588
|
```javascript
|
|
575
|
-
import {
|
|
589
|
+
import {
|
|
590
|
+
formatPrice,
|
|
591
|
+
trackEvent,
|
|
592
|
+
trackPageView,
|
|
593
|
+
trackSiteSearch,
|
|
594
|
+
normalizeTrackingUrl,
|
|
595
|
+
t,
|
|
596
|
+
} from "@eox/pages-theme-eox/src/helpers";
|
|
576
597
|
```
|
|
577
598
|
|
|
578
599
|
### Available Helpers
|
|
@@ -598,12 +619,12 @@ formatPrice(12000, "en"); // "€ 12,000.-"
|
|
|
598
619
|
formatPrice(49.99, "de"); // "€ 49,99"
|
|
599
620
|
```
|
|
600
621
|
|
|
601
|
-
#### `trackEvent(eventDetails)`
|
|
622
|
+
#### `trackEvent(eventDetails, ...rest)`
|
|
602
623
|
|
|
603
|
-
Sends a custom tracking event to Matomo Analytics.
|
|
624
|
+
Sends a custom tracking event to Matomo Analytics. Automatically normalizes any path strings passed in category, action, or name.
|
|
604
625
|
|
|
605
626
|
- **Parameters**:
|
|
606
|
-
- `eventDetails` (`Array<string
|
|
627
|
+
- `eventDetails` (`Array<string|number>|string`): An array `[Category, Action, Name?, Value?]` or positional arguments `(category, action, name, value)`.
|
|
607
628
|
|
|
608
629
|
##### Example Usage:
|
|
609
630
|
|
|
@@ -611,8 +632,51 @@ Sends a custom tracking event to Matomo Analytics.
|
|
|
611
632
|
import { trackEvent } from "@eox/pages-theme-eox/src/helpers";
|
|
612
633
|
|
|
613
634
|
trackEvent(["CTA", "Click", "Technical Specifications", "Viewing Ready"]);
|
|
635
|
+
trackEvent("indicators", "select_indicator", "N1_NO2");
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
#### `trackPageView(options)`
|
|
639
|
+
|
|
640
|
+
Tracks a pageview with normalized path (trailing slash stripped, transient parameters removed) and custom document title.
|
|
641
|
+
|
|
642
|
+
- **Parameters**:
|
|
643
|
+
- `options` (`Object|string`): Vitepress router object, `{ path, title }` options object, or custom path string.
|
|
644
|
+
|
|
645
|
+
##### Example Usage:
|
|
646
|
+
|
|
647
|
+
```javascript
|
|
648
|
+
import { trackPageView } from "@eox/pages-theme-eox/src/helpers";
|
|
649
|
+
|
|
650
|
+
trackPageView({ path: "/explore/", title: "Explore" });
|
|
651
|
+
// Sends normalized URL "/explore" to Matomo
|
|
614
652
|
```
|
|
615
653
|
|
|
654
|
+
#### `trackSiteSearch(keyword, category, resultsCount)`
|
|
655
|
+
|
|
656
|
+
Sends search keyword telemetry to Matomo Site Search reports.
|
|
657
|
+
|
|
658
|
+
- **Parameters**:
|
|
659
|
+
- `keyword` (`string`): Searched term.
|
|
660
|
+
- `category` (`string|boolean`): Category (optional, e.g. `'indicators'`, `'stories'`).
|
|
661
|
+
- `resultsCount` (`number`): Number of search results returned.
|
|
662
|
+
|
|
663
|
+
##### Example Usage:
|
|
664
|
+
|
|
665
|
+
```javascript
|
|
666
|
+
import { trackSiteSearch } from "@eox/pages-theme-eox/src/helpers";
|
|
667
|
+
|
|
668
|
+
trackSiteSearch("methane", "indicators", 4);
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
#### `normalizeTrackingUrl(inputUrl, options)`
|
|
672
|
+
|
|
673
|
+
Pure helper that strips trailing slashes (e.g., `/explore/` -> `/explore`), strips volatile coordinate query params (`x`, `y`, `z`), and sorts remaining query parameters.
|
|
674
|
+
|
|
675
|
+
- **Parameters**:
|
|
676
|
+
- `inputUrl` (`string`): URL or path to normalize.
|
|
677
|
+
- `options` (`Object`): Optional configuration (`ignoredParams`, `stripTrailingSlash`).
|
|
678
|
+
- **Returns**: `string` - Normalized URL string.
|
|
679
|
+
|
|
616
680
|
#### `t(key, i18n)`
|
|
617
681
|
|
|
618
682
|
Retrieves localized translation strings from an i18n mapping object using dot-notation. Falls back to the key itself if no translation is defined.
|
|
@@ -1,7 +1,40 @@
|
|
|
1
1
|
// Mock generic helpers
|
|
2
2
|
// We delegate to a global object to allow spying in tests
|
|
3
3
|
window.__helpersMock = {
|
|
4
|
-
trackEvent: (details) => {},
|
|
4
|
+
trackEvent: (details, ...rest) => {},
|
|
5
|
+
trackPageView: (options) => {},
|
|
6
|
+
trackSiteSearch: (keyword, category, count) => {},
|
|
7
|
+
normalizeTrackingUrl: (url, options) => {
|
|
8
|
+
if (!url || typeof url !== "string") return url;
|
|
9
|
+
const { ignoredParams = ["x", "y", "z"], stripTrailingSlash = true } =
|
|
10
|
+
options || {};
|
|
11
|
+
try {
|
|
12
|
+
const base =
|
|
13
|
+
typeof window !== "undefined" && window.location?.href
|
|
14
|
+
? window.location.href
|
|
15
|
+
: "https://eox.at";
|
|
16
|
+
const u = new URL(url, base);
|
|
17
|
+
let pathname = u.pathname;
|
|
18
|
+
if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
|
|
19
|
+
pathname = pathname.replace(/\/+$/, "");
|
|
20
|
+
}
|
|
21
|
+
const searchParams = new URLSearchParams(u.search);
|
|
22
|
+
for (const key of ignoredParams) {
|
|
23
|
+
searchParams.delete(key);
|
|
24
|
+
}
|
|
25
|
+
searchParams.sort();
|
|
26
|
+
const search = searchParams.toString();
|
|
27
|
+
const isRelative = !/^https?:\/\//i.test(url);
|
|
28
|
+
if (isRelative) {
|
|
29
|
+
return pathname + (search ? `?${search}` : "");
|
|
30
|
+
}
|
|
31
|
+
u.pathname = pathname;
|
|
32
|
+
u.search = search ? `?${search}` : "";
|
|
33
|
+
return u.toString();
|
|
34
|
+
} catch {
|
|
35
|
+
return url;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
5
38
|
acceptCookies: (router) => {},
|
|
6
39
|
declineCookies: (router) => {},
|
|
7
40
|
enableTracking: (enable, router) => {},
|
|
@@ -15,9 +48,31 @@ window.__helpersMock = {
|
|
|
15
48
|
}
|
|
16
49
|
},
|
|
17
50
|
isActive: (item, path) => {
|
|
18
|
-
if (item
|
|
19
|
-
if (item.
|
|
20
|
-
|
|
51
|
+
if (!item || !path) return false;
|
|
52
|
+
if (item.activeMatch) {
|
|
53
|
+
try {
|
|
54
|
+
return new RegExp(item.activeMatch).test(path);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
item.items &&
|
|
61
|
+
item.items.some((sub) => window.__helpersMock.isActive(sub, path))
|
|
62
|
+
) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (item.link) {
|
|
66
|
+
const normalize = (p) =>
|
|
67
|
+
decodeURI(p)
|
|
68
|
+
.replace(/[?#].*$/, "")
|
|
69
|
+
.replace(/\.html$/, "")
|
|
70
|
+
.replace(/\/$/, "");
|
|
71
|
+
const linkPath = normalize(item.link);
|
|
72
|
+
const currentPath = normalize(path);
|
|
73
|
+
if (linkPath === currentPath) return true;
|
|
74
|
+
if (linkPath === "") return currentPath === "";
|
|
75
|
+
return currentPath === linkPath || currentPath.startsWith(linkPath + "/");
|
|
21
76
|
}
|
|
22
77
|
return false;
|
|
23
78
|
},
|
|
@@ -94,6 +149,12 @@ window.__helpersMock = {
|
|
|
94
149
|
};
|
|
95
150
|
|
|
96
151
|
export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
|
|
152
|
+
export const trackPageView = (...args) =>
|
|
153
|
+
window.__helpersMock.trackPageView(...args);
|
|
154
|
+
export const trackSiteSearch = (...args) =>
|
|
155
|
+
window.__helpersMock.trackSiteSearch(...args);
|
|
156
|
+
export const normalizeTrackingUrl = (...args) =>
|
|
157
|
+
window.__helpersMock.normalizeTrackingUrl(...args);
|
|
97
158
|
export const acceptCookies = (...args) =>
|
|
98
159
|
window.__helpersMock.acceptCookies(...args);
|
|
99
160
|
export const declineCookies = (...args) =>
|
package/cypress.config.js
CHANGED
|
@@ -34,6 +34,15 @@ export default defineConfig({
|
|
|
34
34
|
__dirname,
|
|
35
35
|
"cypress/support/mocks/features.data.js",
|
|
36
36
|
),
|
|
37
|
+
// Alias authors.data.js to our mock
|
|
38
|
+
"../authors.data.js": path.resolve(
|
|
39
|
+
__dirname,
|
|
40
|
+
"cypress/support/mocks/authors.data.js",
|
|
41
|
+
),
|
|
42
|
+
"../../authors.data.js": path.resolve(
|
|
43
|
+
__dirname,
|
|
44
|
+
"cypress/support/mocks/authors.data.js",
|
|
45
|
+
),
|
|
37
46
|
},
|
|
38
47
|
},
|
|
39
48
|
plugins: [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.10",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"format": "npx prettier --write .",
|
|
14
14
|
"format:check": "npx prettier --check .",
|
|
15
|
+
"test": "npm run test:unit && npm run test:component",
|
|
16
|
+
"test:unit": "node --test src/helpers.test.js",
|
|
15
17
|
"test:component": "cypress run --component",
|
|
16
18
|
"release": "release-it"
|
|
17
19
|
},
|
package/src/Layout.vue
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
v-if="frontmatter.layout === 'blog'"
|
|
4
|
-
class="layout-blog min-h-screen flex flex-col"
|
|
4
|
+
class="Layout layout-blog min-h-screen flex flex-col"
|
|
5
5
|
>
|
|
6
6
|
<NavBar />
|
|
7
7
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
8
|
+
<HeroSection />
|
|
8
9
|
<main class="flex-grow">
|
|
9
10
|
<BlogList />
|
|
11
|
+
<CTASection
|
|
12
|
+
v-if="frontmatter.cta !== false"
|
|
13
|
+
title="Interested in applying EO data to your use case?"
|
|
14
|
+
tagline="Let's build cloud-native systems, interactive maps, or customized Sentinel data layers together. Reach out to discuss your project requirements."
|
|
15
|
+
primaryButton="Contact us"
|
|
16
|
+
primaryLink="/contact"
|
|
17
|
+
dark
|
|
18
|
+
/>
|
|
10
19
|
</main>
|
|
11
20
|
<Footer v-if="frontmatter.footer !== false" />
|
|
12
21
|
<CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
|
|
13
22
|
</div>
|
|
14
23
|
<div
|
|
15
24
|
v-else-if="frontmatter.layout === 'tags'"
|
|
16
|
-
class="layout-tags min-h-screen flex flex-col"
|
|
25
|
+
class="Layout layout-tags min-h-screen flex flex-col"
|
|
17
26
|
>
|
|
18
27
|
<NavBar />
|
|
19
28
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
29
|
+
<HeroSection />
|
|
20
30
|
<main class="flex-grow">
|
|
21
31
|
<TagList />
|
|
22
32
|
</main>
|
|
@@ -25,10 +35,11 @@
|
|
|
25
35
|
</div>
|
|
26
36
|
<div
|
|
27
37
|
v-else-if="frontmatter.layout === 'team'"
|
|
28
|
-
class="layout-team min-h-screen flex flex-col"
|
|
38
|
+
class="Layout layout-team min-h-screen flex flex-col"
|
|
29
39
|
>
|
|
30
40
|
<NavBar />
|
|
31
41
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
42
|
+
<HeroSection />
|
|
32
43
|
<main class="flex-grow">
|
|
33
44
|
<TeamPage />
|
|
34
45
|
</main>
|
|
@@ -1,45 +1,40 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="blog-landing max-w-[1200px] mx-auto px-6 md:px-8"
|
|
4
|
+
:class="{ 'has-hero': Boolean(frontmatter.hero) }"
|
|
5
|
+
>
|
|
6
|
+
<!-- Page Header (shown only when hero is not present) -->
|
|
7
|
+
<div v-if="!frontmatter.hero" class="blog-landing-header" role="banner">
|
|
5
8
|
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
|
6
9
|
<div>
|
|
7
|
-
<h1 class="blog-landing-title">
|
|
8
|
-
EOX Blog
|
|
9
|
-
</h1>
|
|
10
|
+
<h1 class="blog-landing-title">EOX Blog</h1>
|
|
10
11
|
<p class="blog-landing-subtitle">
|
|
11
12
|
Insights, updates, and deep dives from our geospatial experts.
|
|
12
13
|
</p>
|
|
13
14
|
</div>
|
|
14
15
|
</div>
|
|
16
|
+
</div>
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
<!-- Active Filter Banner -->
|
|
19
|
+
<div v-if="activeTag || activeAuthor" class="active-filter-banner mb-6">
|
|
20
|
+
<span class="filter-label">Active filter:</span>
|
|
21
|
+
<span
|
|
22
|
+
v-if="activeTag"
|
|
23
|
+
:style="getTagStyle(activeTag)"
|
|
24
|
+
class="active-filter-tag shadow-sm inline-flex items-center gap-1.5"
|
|
20
25
|
>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Author: {{ getAuthorName(activeAuthor) }}
|
|
34
|
-
</span>
|
|
35
|
-
<button
|
|
36
|
-
@click="clearFilters"
|
|
37
|
-
class="clear-filter-btn"
|
|
38
|
-
>
|
|
39
|
-
<i class="mdi mdi-close-circle"></i>
|
|
40
|
-
<span>Clear Filter</span>
|
|
41
|
-
</button>
|
|
42
|
-
</div>
|
|
26
|
+
{{ activeTag }}
|
|
27
|
+
</span>
|
|
28
|
+
<span
|
|
29
|
+
v-if="activeAuthor"
|
|
30
|
+
class="active-filter-author shadow-sm inline-flex items-center gap-1.5"
|
|
31
|
+
>
|
|
32
|
+
Author: {{ getAuthorName(activeAuthor) }}
|
|
33
|
+
</span>
|
|
34
|
+
<button @click="clearFilters" class="clear-filter-btn">
|
|
35
|
+
<i class="mdi mdi-close-circle"></i>
|
|
36
|
+
<span>Clear Filter</span>
|
|
37
|
+
</button>
|
|
43
38
|
</div>
|
|
44
39
|
|
|
45
40
|
<!-- Grid of Blog Posts -->
|
|
@@ -52,14 +47,23 @@
|
|
|
52
47
|
:key="post.url"
|
|
53
48
|
:title="post.title"
|
|
54
49
|
:content="post.excerpt"
|
|
55
|
-
:image="
|
|
50
|
+
:image="
|
|
51
|
+
post.coverImage ? getCoverImageUrl(post.coverImage) : undefined
|
|
52
|
+
"
|
|
56
53
|
:date="post.date"
|
|
57
54
|
:authors="post.authors"
|
|
58
|
-
:chips="
|
|
55
|
+
:chips="
|
|
56
|
+
post.tags
|
|
57
|
+
? post.tags.map((t) => ({
|
|
58
|
+
text: t,
|
|
59
|
+
link: withBase(`/tag/${t}/`),
|
|
60
|
+
}))
|
|
61
|
+
: []
|
|
62
|
+
"
|
|
59
63
|
:link="{
|
|
60
64
|
text: 'Read more',
|
|
61
|
-
href: post.url,
|
|
62
|
-
target: '_self'
|
|
65
|
+
href: withBase(post.url),
|
|
66
|
+
target: '_self',
|
|
63
67
|
}"
|
|
64
68
|
variant="cover"
|
|
65
69
|
class="h-full bg-surface-container-low"
|
|
@@ -87,18 +91,18 @@
|
|
|
87
91
|
<!-- Pagination -->
|
|
88
92
|
<nav
|
|
89
93
|
v-if="totalPages > 1"
|
|
90
|
-
class="mt-16 flex justify-center items-center gap-2"
|
|
94
|
+
class="mt-16 flex justify-center items-center gap-1 sm:gap-2"
|
|
91
95
|
>
|
|
92
96
|
<button
|
|
93
97
|
@click="changePage(currentPage - 1)"
|
|
94
98
|
:disabled="currentPage === 1"
|
|
95
|
-
class="
|
|
99
|
+
class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] flex items-center justify-center rounded-xl border border-slate-200 text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors"
|
|
96
100
|
>
|
|
97
101
|
<span class="sr-only">Previous</span>
|
|
98
102
|
<i class="mdi mdi-chevron-left text-lg"></i>
|
|
99
103
|
</button>
|
|
100
104
|
|
|
101
|
-
<div class="flex items-center gap-1.5">
|
|
105
|
+
<div class="flex items-center gap-1 sm:gap-1.5">
|
|
102
106
|
<button
|
|
103
107
|
v-for="page in visiblePages"
|
|
104
108
|
:key="page"
|
|
@@ -108,7 +112,7 @@
|
|
|
108
112
|
? 'bg-primary text-white border-primary shadow-sm'
|
|
109
113
|
: 'border-slate-200 text-slate-600 hover:bg-slate-50',
|
|
110
114
|
]"
|
|
111
|
-
class="min-w-[40px] h-[40px] px-3 font-semibold text-sm rounded-xl border flex items-center justify-center transition-all duration-150"
|
|
115
|
+
class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] px-2 sm:px-3 font-semibold text-sm rounded-xl border flex items-center justify-center transition-all duration-150"
|
|
112
116
|
>
|
|
113
117
|
{{ page }}
|
|
114
118
|
</button>
|
|
@@ -117,7 +121,7 @@
|
|
|
117
121
|
<button
|
|
118
122
|
@click="changePage(currentPage + 1)"
|
|
119
123
|
:disabled="currentPage === totalPages"
|
|
120
|
-
class="
|
|
124
|
+
class="min-w-[32px] sm:min-w-[40px] h-[32px] sm:h-[40px] flex items-center justify-center rounded-xl border border-slate-200 text-slate-600 hover:bg-slate-50 disabled:opacity-40 disabled:hover:bg-transparent disabled:cursor-not-allowed transition-colors"
|
|
121
125
|
>
|
|
122
126
|
<span class="sr-only">Next</span>
|
|
123
127
|
<i class="mdi mdi-chevron-right text-lg"></i>
|
|
@@ -129,9 +133,7 @@
|
|
|
129
133
|
<aside class="blog-sidebar-col">
|
|
130
134
|
<!-- Search widget -->
|
|
131
135
|
<div class="sidebar-widget">
|
|
132
|
-
<h3 class="sidebar-title">
|
|
133
|
-
Search
|
|
134
|
-
</h3>
|
|
136
|
+
<h3 class="sidebar-title">Search</h3>
|
|
135
137
|
<div class="search-container">
|
|
136
138
|
<VPNavBarSearch class="search" />
|
|
137
139
|
</div>
|
|
@@ -140,10 +142,8 @@
|
|
|
140
142
|
<!-- Hot Tags -->
|
|
141
143
|
<div class="sidebar-widget">
|
|
142
144
|
<div class="sidebar-title-container">
|
|
143
|
-
<h3 class="sidebar-title-inline">
|
|
144
|
-
|
|
145
|
-
</h3>
|
|
146
|
-
<a href="/tag" class="see-all-link">
|
|
145
|
+
<h3 class="sidebar-title-inline">Hot tags</h3>
|
|
146
|
+
<a :href="withBase('/tag')" class="see-all-link">
|
|
147
147
|
<span>see all</span>
|
|
148
148
|
<i class="mdi mdi-arrow-right"></i>
|
|
149
149
|
</a>
|
|
@@ -152,7 +152,7 @@
|
|
|
152
152
|
<a
|
|
153
153
|
v-for="tag in calculatedHotTags"
|
|
154
154
|
:key="tag.name"
|
|
155
|
-
:href="`/tag/${tag.name}/`"
|
|
155
|
+
:href="withBase(`/tag/${tag.name}/`)"
|
|
156
156
|
:style="getTagStyle(tag.name)"
|
|
157
157
|
class="sidebar-tag"
|
|
158
158
|
>
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
|
|
168
168
|
<script setup>
|
|
169
169
|
import { ref, computed, onMounted, watch } from "vue";
|
|
170
|
-
import { useRouter, useData } from "vitepress";
|
|
170
|
+
import { useRouter, useData, withBase } from "vitepress";
|
|
171
171
|
import { data as posts } from "../posts.data.js";
|
|
172
172
|
import { data as authorsList } from "../authors.data.js";
|
|
173
173
|
import { getTagColor, getTagStyle } from "../tagUtils.js";
|
|
@@ -198,7 +198,7 @@ const calculatedHotTags = computed(() => {
|
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
const router = useRouter();
|
|
201
|
-
const { params } = useData();
|
|
201
|
+
const { params, frontmatter } = useData();
|
|
202
202
|
|
|
203
203
|
const activeTag = ref("");
|
|
204
204
|
const activeAuthor = ref("");
|
|
@@ -208,10 +208,17 @@ const postsPerPage = 9;
|
|
|
208
208
|
// Form cover image URL correctly (prefixing filename with /images/)
|
|
209
209
|
function getCoverImageUrl(coverImage) {
|
|
210
210
|
if (!coverImage) return "";
|
|
211
|
-
if (coverImage.startsWith("
|
|
211
|
+
if (coverImage.startsWith("http") || coverImage.startsWith("//")) {
|
|
212
212
|
return coverImage;
|
|
213
213
|
}
|
|
214
|
-
|
|
214
|
+
const base = withBase("/");
|
|
215
|
+
if (base && base !== "/" && coverImage.startsWith(base)) {
|
|
216
|
+
return coverImage;
|
|
217
|
+
}
|
|
218
|
+
const path = coverImage.startsWith("/")
|
|
219
|
+
? coverImage
|
|
220
|
+
: `/images/${coverImage}`;
|
|
221
|
+
return withBase(path);
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
const tagFromParam = computed(() => {
|
|
@@ -341,6 +348,10 @@ function clearFilters() {
|
|
|
341
348
|
padding-bottom: 48px !important;
|
|
342
349
|
}
|
|
343
350
|
|
|
351
|
+
.blog-landing.has-hero {
|
|
352
|
+
padding-top: 2rem !important;
|
|
353
|
+
}
|
|
354
|
+
|
|
344
355
|
@media (min-width: 640px) {
|
|
345
356
|
.blog-landing {
|
|
346
357
|
padding-left: 1.5rem !important;
|
|
@@ -348,6 +359,9 @@ function clearFilters() {
|
|
|
348
359
|
padding-top: calc(var(--vp-nav-height) + 40px) !important;
|
|
349
360
|
padding-bottom: 64px !important;
|
|
350
361
|
}
|
|
362
|
+
.blog-landing.has-hero {
|
|
363
|
+
padding-top: 4.5rem !important;
|
|
364
|
+
}
|
|
351
365
|
}
|
|
352
366
|
|
|
353
367
|
@media (min-width: 960px) {
|
|
@@ -357,6 +371,9 @@ function clearFilters() {
|
|
|
357
371
|
padding-top: calc(var(--vp-nav-height) + 58px) !important;
|
|
358
372
|
padding-bottom: 96px !important;
|
|
359
373
|
}
|
|
374
|
+
.blog-landing.has-hero {
|
|
375
|
+
padding-top: 8rem !important;
|
|
376
|
+
}
|
|
360
377
|
}
|
|
361
378
|
|
|
362
379
|
.blog-landing-header {
|
|
@@ -522,7 +539,9 @@ function clearFilters() {
|
|
|
522
539
|
border-radius: 4px !important;
|
|
523
540
|
text-decoration: none !important;
|
|
524
541
|
line-height: 1.2 !important;
|
|
525
|
-
transition:
|
|
542
|
+
transition:
|
|
543
|
+
opacity 0.2s ease,
|
|
544
|
+
transform 0.1s ease !important;
|
|
526
545
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
|
|
527
546
|
}
|
|
528
547
|
|
|
@@ -584,7 +603,7 @@ function clearFilters() {
|
|
|
584
603
|
padding: 0 !important;
|
|
585
604
|
margin: 0 !important;
|
|
586
605
|
margin-left: 0.5rem !important;
|
|
587
|
-
|
|
606
|
+
|
|
588
607
|
display: inline-flex !important;
|
|
589
608
|
align-items: center !important;
|
|
590
609
|
gap: 0.375rem !important;
|