Skip to content

Commit 39918e1

Browse files
Merge pull request #39 from nick-jy-huang/qa
feat: update README.md
2 parents e01e4e4 + d570131 commit 39918e1

8 files changed

Lines changed: 65 additions & 67 deletions

File tree

README.md

Lines changed: 56 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,97 +2,98 @@
22
<img src="https://quotation-app-zeta.vercel.app/favicon.png" width="120" alt="Quotation App Logo">
33
</p>
44

5-
<h1 align="center">Quotation App</h1>
6-
7-
#### 一款簡單好用的線上報價單編輯與預覽工具,支援多欄位填寫、即時計算與美觀的預覽畫面,適合各類型專案報價需求。
5+
<h1 align="center">Quotation App 👨‍💻</h1>
6+
<p align="center">
7+
一款簡單好用的線上報價單編輯與預覽工具,支援多欄位填寫、即時計算與美觀的預覽畫面,適合各類型專案報價需求。
8+
</p>
89

910
<p align="center">
10-
<img src="https://quotation-app-zeta.vercel.app/image.png" width="90%" alt="Quotation App Logo">
11+
<img src="https://quotation-app-zeta.vercel.app/image.jpg" width="90%" alt="Quotation App Preview" style="border-radius: 20px; box-shadow: 0 4px 24px rgba(0,0,0,0.15);" />
1112
</p>
1213

13-
## 特色功能
14-
15-
- 報價單編輯與即時預覽
16-
- 輕鬆填寫基本資料、接案人資料、客戶資料與工作內容
17-
- 報價單小計、總計自動計算
18-
- 支援 Input Debounce,提升輸入體驗
19-
- 社群分享自動顯示預覽圖(Open Graph)
20-
- Axe-core 驗證無障礙設計
21-
- 匯出 PDF 並自動儲存匯出紀錄於 localStorage,支援歷史紀錄載入與清除
22-
- RWD 浮動匯出按鈕與匯出紀錄,手機/平板體驗佳
23-
- Prettier 程式碼格式化與 TypeScript 嚴謹型別
24-
- 元件結構清楚,易於維護與擴充
25-
- 多語系支援
26-
27-
## 技術架構
28-
29-
- Next.js 15
30-
- React 18
31-
- TypeScript
32-
- Tailwind CSS
33-
- next-intl
34-
- Zustand 狀態管理
35-
- dayjs 處理日期
36-
- html2canvas + jsPDF 匯出 PDF
37-
- axe-core 無障礙檢查
38-
39-
## i18n 多語系支援
40-
41-
- 支援中英文(zh-TW / en-US)介面切換
42-
- 語言切換按鈕可即時切換所有 UI 文字
43-
- 語系路由自動化,middleware 會根據支援語言自動產生白名單
44-
- 所有主要元件皆有多語系測試,確保不同語言下顯示正確
45-
- 新增語言只需於 `constants/locale.ts` 設定,無需手動調整 middleware
46-
- message 新增語系表
47-
48-
## 安裝與啟動
49-
50-
1. 下載專案
14+
## 特色功能
15+
16+
- 📝 報價單編輯與即時預覽
17+
- 🧑‍💼 輕鬆填寫基本資料、接案人資料、客戶資料與工作內容
18+
- 🧮 報價單小計、總計自動計算
19+
- 支援 Input Debounce,提升輸入體驗
20+
- 🌐 社群分享自動顯示預覽圖(Open Graph)
21+
- Axe-core 驗證無障礙設計
22+
- 📄 匯出 PDF 並自動儲存匯出紀錄於 localStorage,支援歷史紀錄載入與清除
23+
- 📱 RWD 浮動匯出按鈕與匯出紀錄,手機/平板體驗佳
24+
- 🎨 Prettier 程式碼格式化與 TypeScript 嚴謹型別
25+
- 🧩 元件結構清楚,易於維護與擴充
26+
- 🌏 多語系支援
27+
28+
## 🛠️ 技術架構
29+
30+
- Next.js 15
31+
- ⚛️ React 18
32+
- 🦾 TypeScript
33+
- 💨 Tailwind CSS
34+
- 🌍 next-intl
35+
- 🗃️ Zustand 狀態管理
36+
- 📅 dayjs 處理日期
37+
- 🖨️ html2canvas + jsPDF 匯出 PDF
38+
- axe-core 無障礙檢查
39+
40+
## 🌏 i18n 多語系支援
41+
42+
- 🇹🇼🇺🇸 支援中英文(zh-TW / en-US)介面切換
43+
- 🔄 語言切換按鈕可即時切換所有 UI 文字
44+
- 🛣️ 語系路由自動化,middleware 會根據支援語言自動產生白名單
45+
- 所有主要元件皆有多語系測試,確保不同語言下顯示正確
46+
- 新增語言只需於 `constants/locale.ts` 設定,無需手動調整 middleware
47+
- 🗂️ message 新增語系表
48+
49+
## 🚀 安裝與啟動
50+
51+
1. ⬇️ 下載專案
5152

5253
```bash
5354
git clone https://github.com/nick-jy-huang/quotation-app.git
5455
cd quotation-app
5556
```
5657

57-
2. 安裝相依套件
58+
2. 📦 安裝相依套件
5859

5960
```bash
6061
pnpm install
6162
```
6263

63-
3. 啟動開發伺服器
64+
3. ▶️ 啟動開發伺服器
6465

6566
```bash
6667
pnpm dev
6768
```
6869

69-
4. 開啟瀏覽器並前往 [http://localhost:3000](http://localhost:3000)
70+
4. 🌐 開啟瀏覽器並前往 [http://localhost:3000](http://localhost:3000)
7071

71-
## Node.js 版本需求
72+
## 🖥️ Node.js 版本需求
7273

7374
本專案建議使用 Node.js 20 以上版本(Next.js 15 需 Node.js 18+,建議 20 以上)。
7475

75-
## 測試
76+
## 🧪 測試
7677

7778
本專案採用 Vitest + Testing Library 進行單元與元件測試,涵蓋主要元件、流程、無障礙、RWD、localStorage、副作用等情境。
7879

79-
- 使用 [Vitest](https://vitest.dev/) 作為測試框架,支援 TypeScript 與 Vite 專案。
80-
- 使用 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) 進行元件渲染與互動測試。
81-
- 覆蓋率報表自動產生於 `coverage/` 目錄,可用瀏覽器開啟 `coverage/index.html` 查看詳細覆蓋情況。
82-
- 重要元件皆有 smoke test、props 傳遞、互動、無障礙、RWD 等測試案例。
83-
- PDF 匯出、localStorage 等副作用皆有 mock 處理,確保測試穩定。
80+
- 🧪 使用 [Vitest](https://vitest.dev/) 作為測試框架,支援 TypeScript 與 Vite 專案。
81+
- 🧑‍💻 使用 [@testing-library/react](https://testing-library.com/docs/react-testing-library/intro/) 進行元件渲染與互動測試。
82+
- 📊 覆蓋率報表自動產生於 `coverage/` 目錄,可用瀏覽器開啟 `coverage/index.html` 查看詳細覆蓋情況。
83+
- 重要元件皆有 smoke test、props 傳遞、互動、無障礙、RWD 等測試案例。
84+
- 🗃️ PDF 匯出、localStorage 等副作用皆有 mock 處理,確保測試穩定。
8485

85-
### 執行測試
86+
### 🏃‍♂️ 執行測試
8687

8788
```bash
8889
pnpm test
8990
```
9091

91-
### 相關測試指令
92+
### 🧰 相關測試指令
9293

9394
- `pnpm test`:執行所有單元測試,執行測試並產生覆蓋率報表,**coverage** 資料夾中點擊 **index.html**
9495

95-
## 授權
96+
## 📄 授權
9697

9798
- 本專案採用 MIT License 授權,詳見 [LICENSE](./LICENSE)
9899
- 作者:nick-jy-huang

app/[locale]/layout.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { NextIntlClientProvider } from 'next-intl';
22
import { notFound } from 'next/navigation';
33
import { getTranslations } from 'next-intl/server';
4+
import '@/styles/globals.css';
5+
import SplashScreen from '@/components/SplashScreen';
46

57
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
68
const { locale } = await params;
@@ -16,7 +18,7 @@ export async function generateMetadata({ params }: { params: Promise<{ locale: s
1618
url: 'https://quotation-app-zeta.vercel.app/',
1719
images: [
1820
{
19-
url: 'https://quotation-app-zeta.vercel.app/image.png',
21+
url: 'https://quotation-app-zeta.vercel.app/image.jpg',
2022
width: 1200,
2123
height: 630,
2224
},
@@ -38,13 +40,14 @@ export default async function LocaleLayout({
3840
const { locale } = await params;
3941
let messages;
4042
try {
41-
messages = (await import(`../../messages/${locale}.json`)).default;
43+
messages = (await import(`@/messages/${locale}.json`)).default;
4244
} catch (error) {
4345
notFound();
4446
}
4547

4648
return (
4749
<NextIntlClientProvider locale={locale} messages={messages}>
50+
<SplashScreen />
4851
{children}
4952
</NextIntlClientProvider>
5053
);

app/layout.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Geist, Geist_Mono } from 'next/font/google';
22
import '@/styles/globals.css';
3-
import SplashScreen from '@/components/SplashScreen';
43

54
const geistSans = Geist({
65
variable: '--font-geist-sans',
@@ -12,11 +11,7 @@ const geistMono = Geist_Mono({
1211
subsets: ['latin'],
1312
});
1413

15-
export default function RootLayout({
16-
children,
17-
}: Readonly<{
18-
children: React.ReactNode;
19-
}>) {
14+
export default async function LocaleLayout({ children }: { children: React.ReactNode }) {
2015
return (
2116
<html lang="en">
2217
<head>
@@ -31,7 +26,6 @@ export default function RootLayout({
3126
<body
3227
className={`${geistSans.variable} ${geistMono.variable} bg-grid flex min-h-screen flex-col bg-gray-100`}
3328
>
34-
<SplashScreen />
3529
<main>{children}</main>
3630
</body>
3731
</html>

components/Header/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default function Header({ onChange, activeTab }: HeaderProps) {
1212
<div className="flex items-center justify-between py-4">
1313
<div className="flex gap-4">
1414
<img src="/favicon.png" alt="logo" className="h-8 w-8 duration-300 hover:scale-125" />
15-
<h1 className="hidden text-2xl font-bold text-gray-900 lg:block">Quotation App</h1>
15+
<h1 className="hidden text-2xl font-bold text-gray-900 md:block">Quotation App</h1>
1616
</div>
1717

1818
<div id="desktop-tab-switcher" className="flex items-center gap-2">

i18n/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ export default getRequestConfig(async ({ requestLocale }) => {
1010

1111
return {
1212
locale,
13-
messages: (await import(`../messages/${locale}.json`)).default,
13+
messages: (await import(`@/messages/${locale}.json`)).default,
1414
};
1515
});

middleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export function middleware(request: NextRequest) {
99
}
1010

1111
export const config = {
12-
matcher: ['/((?!_next/static|robots.txt|sitemap.xml|favicon.png|image.png|api).*)', '/'],
12+
matcher: ['/((?!_next/static|robots.txt|sitemap.xml|favicon.png|image.jpg|api).*)', '/'],
1313
};

public/image.jpg

124 KB
Loading

public/image.png

-152 KB
Binary file not shown.

0 commit comments

Comments
 (0)