diff --git a/.gitignore b/.gitignore index 909642c..a430c02 100644 --- a/.gitignore +++ b/.gitignore @@ -6,11 +6,13 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* +*.tsbuildinfo node_modules dist dist-ssr *.local +build # Editor directories and files .vscode/* diff --git a/package.json b/package.json index 6694378..e5ced4c 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", diff --git a/src/About.tsx b/src/About.tsx index c5edd50..30f2a15 100644 --- a/src/About.tsx +++ b/src/About.tsx @@ -1,32 +1,12 @@ import { FC } from 'react'; import { Accordion, Card, Container, Divider, Icon } from 'semantic-ui-react'; import { isMobile } from 'react-device-detect'; +import { DonateInfo, Contributor, FAQ } from './types'; import contributors from './data/processed/contributors.json'; import faqs from './data/processed/faqs.json'; import donateInfo from './data/processed/donateInfo.json'; -interface Contributor { - header: string; - href: string; - as: string; - image: string; - meta: string; - description: string; -} - -interface FAQ { - key: string; - title: string; - content: string; -} - -interface DonateInfo { - image: string; - header: string; - meta: string; -} - const About: FC = () => { return (
diff --git a/src/Files.tsx b/src/Files.tsx index 450d994..c84b282 100644 --- a/src/Files.tsx +++ b/src/Files.tsx @@ -1,13 +1,11 @@ import { FC, useCallback, useEffect, useRef, useReducer, useState } from 'react'; -import { Icon, Label, Pagination, Popup, Search, Table } from 'semantic-ui-react'; +import { Icon, Label, Pagination, Popup, Search, Table, SearchProps } from 'semantic-ui-react'; import * as _ from 'lodash'; -import { File, SearchState } from './types'; +import { File, SearchState, FileHash } from './types'; import files from './data/processed/files.json'; interface HashPopupProps { - hashObj: { - [key: string]: string; - }; + hashObj: FileHash; } const HashPopup: FC = ({ hashObj }) => { @@ -20,9 +18,9 @@ const HashPopup: FC = ({ hashObj }) => { hideOnScroll position='bottom right' > - {Object.keys(hashObj).map((hashType, index) => ( + {Object.keys(hashObj || {}).map((hashType, index) => ( - {hashType}: {hashObj[hashType]} + {hashType}: {hashObj[hashType as keyof FileHash]} ))} @@ -38,7 +36,7 @@ const Files: FC = () => { }); const filesTot = shownFiles.length; - const pagesTot = Math.floor(filesTot / pageLength) + (filesTot % pageLength !== 0); + const pagesTot = Math.floor(filesTot / pageLength) + (filesTot % pageLength !== 0 ? 1 : 0); const leftNum = (activePage - 1) * pageLength; shownFiles.forEach((item, index) => { @@ -70,20 +68,20 @@ const Files: FC = () => { const [searchState, searchDispatch] = useReducer(searchReducer, searchInitState); const { loading, searchResults, value } = searchState; - const timeoutRef = useRef(); + const timeoutRef = useRef(undefined); - const search = useCallback((e: any, data: { value: string }) => { + const search = useCallback((_e: React.MouseEvent, data: SearchProps) => { clearTimeout(timeoutRef.current); - searchDispatch({ type: 'SEARCH_START', query: data.value }); + searchDispatch({ type: 'SEARCH_START', query: data.value || '' }); timeoutRef.current = setTimeout(() => { - if (data.value.length === 0) { + if (!data.value || data.value.length === 0) { searchDispatch({ type: 'SEARCH_CLEAN' }); return; } const isMatch = (item: File) => { - const re = new RegExp(_.escapeRegExp(data.value), 'i'); + const re = new RegExp(_.escapeRegExp(data.value || ''), 'i'); return re.test(item.filename); }; @@ -101,7 +99,9 @@ const Files: FC = () => { useEffect(() => { return () => { - clearTimeout(timeoutRef.current); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } }; }, []); @@ -119,7 +119,7 @@ const Files: FC = () => { onSearchChange={search} results={searchResults} value={value} - onResultSelect={(e, data) => { + onResultSelect={(_e, data) => { searchDispatch({ type: 'SEARCH_UPDATE_SELECTION', selection: data.result }); }} /> @@ -131,12 +131,11 @@ const Files: FC = () => { {shownFiles.slice(leftNum, leftNum + pageLength).map((item, index) => ( - + {item.filename} @@ -144,14 +143,14 @@ const Files: FC = () => { {Object.keys(item.tags).map((tag) => { - switch (tag) { - case 'hash': - return ; - case 'id': - return null; - default: - return ; + if (tag === 'hash') { + return ; + } + if (tag === 'id') { + return null; } + const value = item.tags[tag as keyof typeof item.tags]; + return ; })} @@ -164,7 +163,7 @@ const Files: FC = () => { { + onPageChange={(_e, data) => { setActivePage(data.activePage as number); }} /> diff --git a/src/Header.tsx b/src/Header.tsx index 933b534..e952411 100644 --- a/src/Header.tsx +++ b/src/Header.tsx @@ -1,7 +1,6 @@ import { FC, useState, useEffect } from 'react'; import { Container, Dropdown, Menu } from 'semantic-ui-react'; import { useLocation } from 'react-router-dom'; -import { Link } from './types'; import links from './data/processed/links.json'; const Header: FC = () => { diff --git a/src/Home.tsx b/src/Home.tsx index 7944274..b8d7795 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -1,10 +1,11 @@ import { FC, useEffect, useReducer } from 'react'; import { EventEmitter } from 'events'; import { Container, Divider, Grid, Header, Icon, List, Modal, Segment } from 'semantic-ui-react'; -import { Software, HomeModalState } from './types'; +import { Software, HomeModalState, FileSource } from './types'; import softwareData from './data/processed/software.json'; -const softwareSlug = (softwareData as Software[]).map((item) => item.slug); +const typedSoftwareData = (softwareData as unknown) as Software[]; +const softwareSlug = typedSoftwareData.map((item) => item.slug); const eventEmitter = new EventEmitter(); interface SoftwareCardProps { @@ -35,7 +36,7 @@ const SoftwareList: FC = () => { return ( - {(softwareData as Software[]) + {typedSoftwareData .filter((item) => !item.recommend) .map((item) => ( @@ -48,12 +49,14 @@ const SoftwareList: FC = () => { const HomeModal: FC = () => { const modalReducer = (state: HomeModalState, action: { type: string; slug?: string }) => { switch (action.type) { - case 'open': + case 'open': { + const software = typedSoftwareData[softwareSlug.indexOf(action.slug || '')]; return { ...state, open: true, - download: softwareData[softwareSlug.indexOf(action.slug || '')].sources + download: software ? software.sources : {} }; + } case 'close': default: return { ...state, open: false, download: {} }; @@ -63,114 +66,88 @@ const HomeModal: FC = () => { const [state, dispatch] = useReducer(modalReducer, { open: false, download: {} }); useEffect(() => { - const listener = eventEmitter.addListener('openDownloadModal', (data) => { + const handler = (data: { type: string; slug?: string }) => { dispatch(data); - }); + }; + eventEmitter.addListener('openDownloadModal', handler); return () => { - listener.remove(); + eventEmitter.removeListener('openDownloadModal', handler); }; }, []); return ( - dispatch({type: 'close'})} - > - 选择下载地址 - - - - { - Object.keys(state.download).map((key, index) => { - return ( - - {key} - - { - state.download[key].map((item, index) => { - return ( - - {item.filename} - - ); - }) - } - - - ); - }) - } - - - - + dispatch({type: 'close'})} + > + 选择下载地址 + + + + {Object.entries(state.download).map(([key, sources], index) => ( + + {key} + + {(sources as FileSource[]).map((item, itemIndex) => ( + + {item.filename} + + ))} + + + ))} + + + + ); }; const Home: FC = () => { return ( -
- -
- - - 欢迎 - - 这是一个搜集常用软件镜像下载地址的列表(仅针对中国大陆用户) - - -
- {/* - - - - - 需要帮助 - - - 提交 - 建议&问题 - 提交 Github - Issues - (国内可能无法正常打开) - 查阅 FAQ - - - - */} -
- -
+
+ +
+ + + 欢迎 + + 这是一个搜集常用软件镜像下载地址的列表(仅针对中国大陆用户) + + +
+
+ +
); }; diff --git a/src/main.tsx b/src/main.tsx index 94baa60..2721e6d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; import reportWebVitals from './reportWebVitals'; import 'fomantic-ui-css/semantic.min.css'; @@ -45,11 +45,11 @@ const App: React.FC = () => { ); }; -ReactDOM.render( +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( - , - document.getElementById('root') + ); reportWebVitals((metric) => console.log(metric)); \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index f6386c7..c90a007 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,29 @@ +export interface FileSource { + filename: string; + url: string; +} + +export interface FileHash { + md5?: string; + sha1?: string; + sha256?: string; +} + +export interface FileTags { + source: string; + id: string; + filetype: string; + hash?: FileHash; +} + +export interface File { + filename: string; + url: string; + urlType: "directly" | "multiple"; + tags: FileTags; + index?: number; +} + export interface Software { name: string; website: string; @@ -6,28 +32,8 @@ export interface Software { recommend: boolean; slug: string; sources: { - [key: string]: { - filename: string; - url: string; - }[]; - }; -} - -export interface File { - filename: string; - url: string; - urlType: string; - tags: { - source: string; - id: string; - filetype: string; - hash?: { - md5: string; - sha1: string; - sha256: string; - }; + [key: string]: FileSource[]; }; - index?: number; } export interface Contributor { @@ -57,44 +63,6 @@ export interface BuildInfo { time: string; } -export interface Software { - name: string; - website: string; - description: string; - filesId: string[]; - recommend: boolean; - slug: string; - sources: { - [key: string]: FileSource[]; - }; -} - -export interface FileSource { - filename: string; - url: string; -} - -export interface FileHash { - md5?: string; - sha1?: string; - sha256?: string; -} - -export interface FileTags { - source: string; - id: string; - filetype: string; - hash?: FileHash; -} - -export interface File { - filename: string; - url: string; - urlType: "directly" | "multiple"; - tags: FileTags; - index?: number; -} - export interface SearchState { loading: boolean; searchResults: SearchResult[]; @@ -113,4 +81,11 @@ export interface HomeModalState { download: { [key: string]: FileSource[]; }; +} + +export interface DonateInfo { + header: string; + as: string; + image: string; + meta: string; } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 89d2973..33b04a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -624,22 +624,16 @@ dependencies: undici-types "~6.20.0" -"@types/prop-types@*": - version "15.7.14" - resolved "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2" - integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ== - -"@types/react-dom@^18.3.5": - version "18.3.5" - resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.5.tgz#45f9f87398c5dcea085b715c58ddcf1faf65f716" - integrity sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q== - -"@types/react@^18.3.18": - version "18.3.18" - resolved "https://registry.npmmirror.com/@types/react/-/react-18.3.18.tgz#9b382c4cd32e13e463f97df07c2ee3bbcd26904b" - integrity sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ== - dependencies: - "@types/prop-types" "*" +"@types/react-dom@^19.0.2": + version "19.0.2" + resolved "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.0.2.tgz#ad21f9a1ee881817995fd3f7fd33659c87e7b1b7" + integrity sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg== + +"@types/react@^19.0.2": + version "19.0.2" + resolved "https://registry.npmmirror.com/@types/react/-/react-19.0.2.tgz#9363e6b3ef898c471cb182dd269decc4afc1b4f6" + integrity sha512-USU8ZI/xyKJwFTpjSVIrSeHBVAGagkHQKPNbxeWwql/vDmnTIBgx+TJnhFnj1NXgz8XfprU0egV2dROLGpsBEg== + dependencies: csstype "^3.0.2" "@typescript-eslint/eslint-plugin@8.18.2":