Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Binary file added public/demo/stage-profile-en.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/demo/stage-profile-zh.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 51 additions & 0 deletions src/components/Hero.astro
Original file line number Diff line number Diff line change
@@ -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';
---
<section class="mb-14 overflow-hidden rounded-[var(--radius-card)] border border-edge bg-surface">
<div class="grid gap-8 p-6 sm:p-10 lg:grid-cols-[1.05fr_1fr] lg:items-center">
<div>
<p class="mb-3 text-xs font-medium uppercase tracking-wider text-accent">{t.eyebrow}</p>
<h1 class="font-serif text-3xl leading-tight text-ink sm:text-4xl">{t.heading}</h1>
<p class="mt-4 text-muted">{t.body}</p>
<div class="mt-6 flex flex-wrap items-center gap-4">
<a
href={toolHref}
class="rounded-lg bg-accent px-5 py-2.5 text-sm font-medium text-white transition-opacity hover:opacity-90"
>{t.cta}</a>
<a href="#cycling" class="text-sm text-muted underline-offset-4 hover:text-accent hover:underline">{t.secondary}</a>
</div>
</div>
<a href={toolHref} class="block overflow-hidden rounded-lg border border-edge" aria-label={t.gifAlt}>
<img src={gifSrc} alt={t.gifAlt} width="880" height="560" loading="eager" class="w-full" />
</a>
</div>
</section>
47 changes: 36 additions & 11 deletions src/components/HomeGrid.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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];
---
<SearchBar client:idle locale={locale} />
<div id="all-tools">
{CATEGORY_ORDER.map((cat) => {
const inCat = available.filter((t) => t.category === cat);
if (inCat.length === 0) return null;
return (
<section class="mb-10">
<h2 class="mb-4 font-serif text-xl text-ink">{categoryLabel(cat, locale)}</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{inCat.map((t) => <ToolCard tool={t} locale={locale} />)}
{cycling.length > 0 && (
<section class="mb-12" id="cycling">
<h2 class="mb-1 font-serif text-xl text-ink">{categoryLabel('單車', locale)}</h2>
<p class="mb-4 text-sm text-muted">{t.cyclingIntro}</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{cycling.map((tool) => <ToolCard tool={tool} locale={locale} />)}
</div>
</section>
)}

<section class="mb-10">
<h2 class="mb-4 text-xs font-medium uppercase tracking-wider text-muted">{t.more}</h2>
{restCategories.map((cat) => {
const inCat = available.filter((t2) => t2.category === cat);
if (inCat.length === 0) return null;
return (
<div class="mb-5">
<h3 class="mb-2 text-sm text-muted">{categoryLabel(cat, locale)}</h3>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{inCat.map((tool) => <ToolCard tool={tool} locale={locale} compact />)}
</div>
</div>
</section>
);
})}
);
})}
</section>
</div>
<script>
// 靜態卡片星星:單一委派監聽器驅動全部星星,與 SearchBar island 靠事件同步
Expand Down
50 changes: 36 additions & 14 deletions src/components/ToolCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,48 @@ import { toolTitle, toolDescription, toolPath, type Locale } from '../lib/i18n';

// 純伺服器端卡片:星星是普通按鈕,由 index.astro 的委派腳本驅動
// (不用 29 個 React islands,首頁保持輕量)
// compact:首頁「更多小工具」用的密集樣式(陳列級工具降權,但連結與收藏功能不變)
interface Props {
tool: Tool;
locale?: Locale;
compact?: boolean;
}
const { tool, locale = 'zh' } = Astro.props;
const { tool, locale = 'zh', compact = false } = Astro.props;
---
<a
href={toolPath(tool, locale)}
class="group relative block rounded-[var(--radius-card)] border border-edge bg-surface p-5 transition-all hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_8px_24px_-12px_rgba(0,0,0,0.18)]"
class={compact
? 'group relative flex items-center gap-2.5 rounded-lg border border-edge bg-surface px-3 py-2.5 transition-colors hover:border-accent/40'
: 'group relative block rounded-[var(--radius-card)] border border-edge bg-surface p-5 transition-all hover:-translate-y-0.5 hover:border-accent/40 hover:shadow-[0_8px_24px_-12px_rgba(0,0,0,0.18)]'}
>
<div class="mb-3 flex items-center justify-between">
<span class="text-2xl">{tool.icon}</span>
<button
type="button"
class="js-fav text-base leading-none transition-transform hover:scale-110"
data-id={tool.id}
aria-label={locale === 'en' ? 'Pin tool' : '釘選工具'}
aria-pressed="false"
>☆</button>
</div>
<h3 class="font-serif text-lg text-ink">{toolTitle(tool, locale)}</h3>
<p class="mt-1 text-sm text-muted">{toolDescription(tool, locale)}</p>
{compact ? (
<>
<span class="order-1 text-lg">{tool.icon}</span>
{/* 星星在標題「前面」出圖:DOM 順序避開跟命令面板結果列同名子字串衝突(星星的 aria-label
會併進連結的無障礙名稱),視覺順序仍在最後——與非 compact 卡片的既有結構一致 */}
<button
type="button"
class="js-fav order-3 shrink-0 text-sm leading-none transition-transform hover:scale-110"
data-id={tool.id}
aria-label={locale === 'en' ? 'Pin tool' : '釘選工具'}
aria-pressed="false"
>☆</button>
<span class="order-2 flex-1 truncate text-sm text-ink">{toolTitle(tool, locale)}</span>
</>
) : (
<>
<div class="mb-3 flex items-center justify-between">
<span class="text-2xl">{tool.icon}</span>
<button
type="button"
class="js-fav text-base leading-none transition-transform hover:scale-110"
data-id={tool.id}
aria-label={locale === 'en' ? 'Pin tool' : '釘選工具'}
aria-pressed="false"
>☆</button>
</div>
<h3 class="font-serif text-lg text-ink">{toolTitle(tool, locale)}</h3>
<p class="mt-1 text-sm text-muted">{toolDescription(tool, locale)}</p>
</>
)}
</a>
11 changes: 6 additions & 5 deletions src/pages/en/index.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import Hero from '../../components/Hero.astro';
import HomeGrid from '../../components/HomeGrid.astro';
---
<BaseLayout>
<section class="mb-10">
<h1 class="font-serif text-4xl text-ink">Tiglet</h1>
<p class="mt-2 text-muted">A clean, no-login collection of small browser tools.</p>
</section>
<BaseLayout
title="Tiglet — Turn your route into a stage chart"
description="Upload a GPX / FIT / TCX for a Tour-style stage profile with auto-categorized climbs, plus a cycling toolkit — no login, runs entirely in your browser."
>
<Hero locale="en" />
<HomeGrid locale="en" />
</BaseLayout>
11 changes: 6 additions & 5 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Hero from '../components/Hero.astro';
import HomeGrid from '../components/HomeGrid.astro';
---
<BaseLayout>
<section class="mb-10">
<h1 class="font-serif text-4xl text-ink">Tiglet</h1>
<p class="mt-2 text-muted">一組乾淨、免登入的瀏覽器小工具。</p>
</section>
<BaseLayout
title="Tiglet — 把路線變成賽段剖面圖"
description="上傳 GPX/FIT/TCX,自動生成環法風格賽段剖面圖,爬坡自動分級;還有發票對獎、中油油價等台灣在地小工具與齒比、胎壓等單車工具箱,全部免登入、在瀏覽器裡完成。"
>
<Hero locale="zh" />
<HomeGrid locale="zh" />
</BaseLayout>
Loading