diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts index 75847f1..d41145c 100644 --- a/e2e/home.spec.ts +++ b/e2e/home.spec.ts @@ -11,6 +11,24 @@ test('首頁 HTML 伺服器端就包含全部工具連結(SEO)', async ({ re expect(html).toContain('齒比計算器'); }); +test('首頁 Hero 主打賽段剖面圖,且賽段剖面圖不在單車格線裡重複出現', async ({ page }) => { + await page.goto('/'); + await waitForIslands(page); + + await expect(page.getByRole('heading', { name: '把你的路線變成環法風格賽段圖', level: 1 })).toBeVisible(); + const heroCta = page.getByRole('link', { name: '上傳 GPX,立即製作' }); + await expect(heroCta).toHaveAttribute('href', '/tools/stage-profile'); + + // 錨點連到下方單車工具箱 + await page.getByRole('link', { name: '或看看單車工具箱 ↓' }).click(); + await expect(page).toHaveURL(/#cycling$/); + + // 賽段剖面圖已是 Hero 主角,單車工具箱裡不再重複列出 + const cyclingSection = page.locator('#cycling'); + await expect(cyclingSection.getByRole('link', { name: /賽段剖面圖/ })).toHaveCount(0); + await expect(cyclingSection.getByRole('link', { name: /齒比計算器/ })).toBeVisible(); +}); + test('搜尋會過濾工具格,清空後還原', async ({ page }) => { await page.goto('/'); await waitForIslands(page); diff --git a/public/demo/stage-profile-en.gif b/public/demo/stage-profile-en.gif new file mode 100644 index 0000000..2280149 Binary files /dev/null and b/public/demo/stage-profile-en.gif differ diff --git a/public/demo/stage-profile-zh.gif b/public/demo/stage-profile-zh.gif new file mode 100644 index 0000000..c2a4664 Binary files /dev/null and b/public/demo/stage-profile-zh.gif differ diff --git a/src/components/Hero.astro b/src/components/Hero.astro new file mode 100644 index 0000000..c3e2316 --- /dev/null +++ b/src/components/Hero.astro @@ -0,0 +1,51 @@ +--- +import type { Locale } from '../lib/i18n'; + +// 首頁招牌區:把賽段剖面圖從「36 分之 1」拉出來當全站主軸。 +// 其餘工具在 HomeGrid 分層退場,不在這裡搶焦點。 +interface Props { + locale: Locale; +} +const { locale } = Astro.props; + +const L = { + zh: { + eyebrow: '🚴 單車 · 賽段剖面圖', + heading: '把你的路線變成環法風格賽段圖', + body: '上傳 GPX/FIT/TCX,爬坡自動偵測分級,坡度色帶、地標、每公里細部圖一次生成——像武嶺、KOM 這種硬爬坡,一眼看懂全程配速。', + cta: '上傳 GPX,立即製作', + secondary: '或看看單車工具箱 ↓', + gifAlt: '賽段剖面圖示範:上傳路線、游標讀數、深色主題切換', + }, + en: { + eyebrow: '🚴 Cycling · Stage Profile', + heading: 'Turn your route into a Tour-style stage chart', + body: 'Upload a GPX / FIT / TCX and get auto-categorized climbs, gradient color bands, landmarks and per-km detail — read a brutal climb like Wuling or a KOM segment at a glance.', + cta: 'Upload a GPX, try it now', + secondary: 'Or see the cycling toolkit ↓', + gifAlt: 'Stage Profile demo: route upload, hover readout, dark theme switch', + }, +} as const; +const t = L[locale]; +const toolHref = locale === 'en' ? '/en/tools/stage-profile' : '/tools/stage-profile'; +const gifSrc = locale === 'en' ? '/demo/stage-profile-en.gif' : '/demo/stage-profile-zh.gif'; +--- +
+
+
+

{t.eyebrow}

+

{t.heading}

+

{t.body}

+ +
+ + {t.gifAlt} + +
+
diff --git a/src/components/HomeGrid.astro b/src/components/HomeGrid.astro index 7856dba..8879f9b 100644 --- a/src/components/HomeGrid.astro +++ b/src/components/HomeGrid.astro @@ -6,26 +6,51 @@ import { categoryLabel, type Locale } from '../lib/i18n'; // 首頁主體(zh 與 /en/ 共用):工具卡片伺服器端輸出(SEO), // SearchBar island 負責搜尋過濾與最愛/最近。 +// +// 分層陳列:單車工具箱緊跟 Hero(賽段剖面圖的配角),完整卡片; +// 其餘分類收進「更多小工具」用密集列表降權——連結與收藏功能不變,只是不搶焦點。 interface Props { locale: Locale; } const { locale } = Astro.props; const available = tools.filter((t) => t.status === 'available'); +// 賽段剖面圖已經是 Hero 的主角,這裡不再重複列出 +const cycling = available.filter((t) => t.category === '單車' && t.id !== 'stage-profile'); +const restCategories = CATEGORY_ORDER.filter((c) => c !== '單車'); + +const L = { + zh: { cyclingIntro: '賽段剖面圖之外,這些工具幫你把訓練、補給與胎壓也一次算好。', more: '更多小工具' }, + en: { cyclingIntro: 'Beyond the stage profile, these tools cover training, fueling and tire pressure.', more: 'More tools' }, +} as const; +const t = L[locale]; ---
- {CATEGORY_ORDER.map((cat) => { - const inCat = available.filter((t) => t.category === cat); - if (inCat.length === 0) return null; - return ( -
-

{categoryLabel(cat, locale)}

-
- {inCat.map((t) => )} + {cycling.length > 0 && ( +
+

{categoryLabel('單車', locale)}

+

{t.cyclingIntro}

+
+ {cycling.map((tool) => )} +
+
+ )} + +
+

{t.more}

+ {restCategories.map((cat) => { + const inCat = available.filter((t2) => t2.category === cat); + if (inCat.length === 0) return null; + return ( +
+

{categoryLabel(cat, locale)}

+
+ {inCat.map((tool) => )} +
-
- ); - })} + ); + })} +