Skip to content

Commit ec7f1b9

Browse files
authored
Merge pull request #4 from nk2028/dev
Update UI
1 parent 5a3fbe4 commit ec7f1b9

File tree

10 files changed

+206
-271
lines changed

10 files changed

+206
-271
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@
2424
pnpm install
2525

2626
# 啟動開發服務器
27-
pnpm next dev
27+
pnpm dev
2828

2929
# 構建生產版本
30-
pnpm next build
30+
pnpm build
3131

3232
# 預覽生產構建
33-
pnpm next start
33+
pnpm start
3434
```
3535

3636
## GitHub 仓庫

src/app/NotoTraditionalNushu.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/* https://github.com/nushu-script/NotoTraditionalNushu */
2+
3+
:lang(zh-Nshu) {
4+
font-family: "Noto Traditional Nushu", sans-serif, sans-serif;
5+
}

src/app/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@import "./scrollbar.css";
33
@import "./CharisSIL.css";
44
@import "./SBBWeb.css";
5+
@import "./NotoTraditionalNushu.css";
56

67
/* Swiss SBB Modern Style - Design Tokens
78
* Movement: Contemporary Swiss Design (2020s SBB branding)

src/app/layout.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export default function RootLayout({
3333
return (
3434
<html lang="zh-HK">
3535
<head>
36+
<link
37+
rel="stylesheet"
38+
href="https://cdn.jsdelivr.net/gh/nushu-script/NotoTraditionalNushu@woff-v1.0/index.css"
39+
/>
3640
<Script
3741
src="https://static.cloudflareinsights.com/beacon.min.js"
3842
strategy="afterInteractive"

src/components/Query.tsx

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"use client";
22

3+
import React from "react";
4+
35
import { queryCharacters } from "@/lib/api";
4-
import { buildTableRows, parse廣韻字音, parse中原音韻字音, parse東干甘肅話字音 } from "@/lib/dataProcessor";
5-
import type { CharacterResult, ProcessedLanguage, TableRow } from "@/types";
6+
import { buildTableRows, parse特殊語言字音 } from "@/lib/dataProcessor";
7+
import { CharacterResultItem, ProcessedLanguage, TableRow, UserSettings } from "@/types";
68
import { useState, useEffect } from "react";
79
import { useApp } from "@/contexts/AppContext";
810
import { getTranslation } from "@/lib/i18n";
@@ -31,6 +33,92 @@ const MagnifyingGlassIcon = () => (
3133
</svg>
3234
);
3335

36+
const SpecialCharReadingFragments = ({
37+
parsedList,
38+
}: {
39+
parsedList: { field: string; fieldValue: string; lang: string }[];
40+
}) => {
41+
return (
42+
<>
43+
{parsedList.map((item, idx) => (
44+
<React.Fragment key={idx}>
45+
<span title={item.field} lang={item.lang}>
46+
{item.fieldValue}
47+
</span>
48+
{idx !== parsedList.length - 1 && "/"}
49+
</React.Fragment>
50+
))}
51+
</>
52+
);
53+
};
54+
55+
const CharReadingBox = ({
56+
字音,
57+
langAbbr,
58+
idx,
59+
settings,
60+
}: {
61+
字音: CharacterResultItem;
62+
langAbbr: string;
63+
idx: number;
64+
settings: UserSettings;
65+
}) => {
66+
const isSpecialLanguage = langAbbr === "廣韻" || langAbbr === "中原音韻" || langAbbr === "東干甘肅話";
67+
const is女書 = langAbbr === "江永上江墟";
68+
69+
if (typeof 字音 === "string") {
70+
return (
71+
<td
72+
key={idx}
73+
{...(isSpecialLanguage ? {} : { lang: "zh-Latn-fonipa" })}
74+
className="border border-border px-2 py-1 text-sm bg-card font-mono break-words overflow-hidden text-foreground"
75+
style={{ width: "192px", maxWidth: "192px", minWidth: "192px" }}>
76+
{isSpecialLanguage ? (
77+
<SpecialCharReadingFragments parsedList={parse特殊語言字音(字音, settings, langAbbr)} />
78+
) : (
79+
字音
80+
)}
81+
</td>
82+
);
83+
}
84+
85+
// 多音字
86+
else if (Array.isArray(字音)) {
87+
return (
88+
<td
89+
key={idx}
90+
className="border border-border px-2 py-1 text-sm bg-card font-mono break-words overflow-hidden text-foreground"
91+
style={{ width: "192px", maxWidth: "192px", minWidth: "192px" }}>
92+
{字音.map((item, idx) => {
93+
const 音標 = item[0];
94+
const 注釋 = item[1] ?? null;
95+
96+
return (
97+
<div key={idx} className="mb-1 last:mb-0">
98+
{isSpecialLanguage ? (
99+
<SpecialCharReadingFragments parsedList={parse特殊語言字音(音標, settings, langAbbr)} />
100+
) : (
101+
<span lang="zh-Latn-fonipa">{音標}</span>
102+
)}
103+
{注釋 && (
104+
<span lang={is女書 ? "zh-Nshu" : "zh-HK"} className="ml-1 text-xs text-muted-foreground">
105+
{注釋}
106+
</span>
107+
)}
108+
</div>
109+
);
110+
})}
111+
</td>
112+
);
113+
}
114+
};
115+
116+
const makeRow = (row: TableRow, settings: UserSettings) => {
117+
return row.字音列表.map((字音: CharacterResultItem, charIdx: number) => {
118+
return <CharReadingBox key={charIdx} 字音={字音} langAbbr={row.languageAbbr} idx={charIdx} settings={settings} />;
119+
});
120+
};
121+
34122
const Query = () => {
35123
const {
36124
processedLanguages,
@@ -91,9 +179,6 @@ const Query = () => {
91179
}
92180
}, [contextQueryResults, processedLanguages, settings.selectedLanguages]);
93181

94-
// Extract characters from query results
95-
const characters = contextQueryResults ? contextQueryResults.map(([char]: CharacterResult) => char) : [];
96-
97182
return (
98183
<div className="bg-background">
99184
{/* Query Input Section */}
@@ -121,7 +206,7 @@ const Query = () => {
121206
</div>
122207

123208
{/* Results Table Section */}
124-
{tableRows.length > 0 && (
209+
{contextQueryResults !== null && tableRows.length > 0 && (
125210
<div className="p-4 flex justify-center">
126211
<div className="overflow-x-auto shadow-sm">
127212
<table className="border-collapse border border-border bg-card">
@@ -130,7 +215,7 @@ const Query = () => {
130215
<th
131216
className="border border-border px-2 py-1 text-left text-sm font-bold bg-[#EB0000]"
132217
style={{ width: "128px", maxWidth: "128px", minWidth: "128px" }}></th>
133-
{characters.map((char: string, idx: number) => (
218+
{(contextQueryResults[0].slice(1) as string[]).map((char: string, idx: number) => (
134219
<th
135220
key={idx}
136221
className="border border-border px-2 py-1 text-center text-lg font-bold bg-[#EB0000]"
@@ -163,48 +248,7 @@ const Query = () => {
163248
{row.languageAbbr}
164249
</span>
165250
</td>
166-
{characters.map((char: string, charIdx: number) => {
167-
let 字音 = row.字音列表[char] || "—";
168-
let isHTML = false;
169-
// Special handling for Guangyun (廣韻) data
170-
if (row.languageAbbr === "廣韻" && 字音 !== "—") {
171-
字音 = parse廣韻字音(字音, settings.廣韻字段);
172-
isHTML = true; // Guangyun data contains HTML tags
173-
}
174-
175-
// Special handling for 中原音韻 data
176-
if (row.languageAbbr === "中原音韻" && 字音 !== "—") {
177-
字音 = parse中原音韻字音(字音, settings.中原音韻字段);
178-
isHTML = true; // 中原音韻 data contains HTML tags
179-
}
180-
181-
// Special handling for 東干甘肅話 data
182-
if (row.languageAbbr === "東干甘肅話" && 字音 !== "—") {
183-
字音 = parse東干甘肅話字音(字音, settings.東干甘肅話字段);
184-
isHTML = true; // 東干甘肅話 data contains HTML tags
185-
}
186-
187-
// Render with HTML or plain text
188-
if (isHTML) {
189-
return (
190-
<td
191-
key={`char-${charIdx}`}
192-
className="border border-border px-2 py-1 text-sm bg-card font-mono break-words overflow-hidden text-foreground"
193-
style={{ width: "192px", maxWidth: "192px", minWidth: "192px" }}
194-
dangerouslySetInnerHTML={{ __html: 字音 }}
195-
/>
196-
);
197-
}
198-
199-
return (
200-
<td
201-
key={`char-${charIdx}`}
202-
className="border border-border px-2 py-1 text-sm bg-card font-mono break-words overflow-hidden text-foreground"
203-
style={{ width: "192px", maxWidth: "192px", minWidth: "192px" }}>
204-
{字音}
205-
</td>
206-
);
207-
})}
251+
{makeRow(row, settings)}
208252
</tr>
209253
);
210254
})}

src/components/Settings.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,13 @@ export default function Settings() {
4646
}
4747
groups.get(region)!.push(lang);
4848
});
49-
// Sort by minimum sortOrder in each region (sortOrder is already based on current display mode)
49+
50+
// TODO: simplify this sorting logic
5051
return Array.from(groups.entries()).sort(([, a], [, b]) => {
51-
const minSortOrderA = Math.min(...a.map(lang => lang.sortOrder));
52-
const minSortOrderB = Math.min(...b.map(lang => lang.sortOrder));
53-
return minSortOrderA - minSortOrderB;
52+
// Use "龥" as a placeholder for null sort order to ensure it sorts at the end
53+
const minSortOrderA = a.map(lang => lang.sortOrder).sort((x, y) => (x ?? "龥").localeCompare(y ?? "龥"))[0];
54+
const minSortOrderB = b.map(lang => lang.sortOrder).sort((x, y) => (x ?? "龥").localeCompare(y ?? "龥"))[0];
55+
return (minSortOrderA ?? "龥").localeCompare(minSortOrderB ?? "龥");
5456
});
5557
}, [filteredLanguages]);
5658

src/contexts/AppContext.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
Theme,
1313
UserSettings,
1414
Pages,
15-
CharacterResult,
15+
CharacterResultTable,
1616
} from "@/types";
1717
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
1818
import { fetchLanguages } from "@/lib/api";
@@ -46,8 +46,8 @@ interface AppContextValue {
4646
// Query state
4747
queryInput: string;
4848
setQueryInput: (input: string) => void;
49-
queryResults: CharacterResult[] | null;
50-
setQueryResults: (results: CharacterResult[] | null) => void;
49+
queryResults: CharacterResultTable<string[]> | null;
50+
setQueryResults: (results: CharacterResultTable<string[]> | null) => void;
5151
}
5252

5353
const AppContext = createContext<AppContextValue | undefined>(undefined);
@@ -94,7 +94,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
9494
const [isLoadingLanguages, setIsLoadingLanguages] = useState(true);
9595
const [languagesError, setLanguagesError] = useState<Error | null>(null);
9696
const [queryInput, setQueryInput] = useState<string>("");
97-
const [queryResults, setQueryResults] = useState<CharacterResult[] | null>(null);
97+
const [queryResults, setQueryResults] = useState<CharacterResultTable<string[]> | null>(null);
9898
const [page, setPage] = useState<Pages>("query");
9999
const [language, setLanguageState] = useState<Language>(getCachedDisplayLanguage() || DEFAULT_LANGUAGE);
100100
const [settings, setSettings] = useState<UserSettings>(() => {

src/lib/api.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ApiResponse, CharacterResult, LanguageInfo } from "@/types";
1+
import type { ApiResponse, CharacterResultTable, LanguageInfo } from "@/types";
22

33
const API_BASE = "https://1305783649-j61pduj0mx.ap-guangzhou.tencentscf.com";
44
const VERSION_CACHE_KEY = "yindian_api_version";
@@ -135,7 +135,9 @@ export async function fetchLanguages(forceRefresh = false): Promise<LanguageInfo
135135
* Query character pronunciations with version checking
136136
* @param chars - Chinese characters to query (no spaces)
137137
*/
138-
export async function queryCharacters(chars: string): Promise<{ version: string; data: CharacterResult[] }> {
138+
export async function queryCharacters(
139+
chars: string,
140+
): Promise<{ version: string; data: CharacterResultTable<string[]> }> {
139141
try {
140142
const url = `${API_BASE}/chars/?chars=${encodeURIComponent(chars)}`;
141143
console.log("Fetching:", url);
@@ -151,11 +153,11 @@ export async function queryCharacters(chars: string): Promise<{ version: string;
151153
throw new Error(`HTTP error! status: ${response.status}`);
152154
}
153155

154-
const apiResponse = (await response.json()) as ApiResponse<CharacterResult[]>;
156+
const apiResponse = (await response.json()) as ApiResponse<CharacterResultTable<string[]>>;
155157

156158
// Check version and trigger language refresh if needed
157159
const cachedVersion = getCachedVersion();
158-
const currentVersion = String(apiResponse.version); // Ensure version is string for comparison
160+
const currentVersion = apiResponse.version;
159161
if (cachedVersion && cachedVersion !== currentVersion) {
160162
console.log(`Version mismatch detected. Cached: ${cachedVersion}, API: ${currentVersion}`);
161163
// Trigger background refresh of languages

0 commit comments

Comments
 (0)