From a6ae3e267126eae2fbd104a9823fa1c662dcba21 Mon Sep 17 00:00:00 2001 From: jordansilly77-stack Date: Sun, 21 Jun 2026 10:34:10 +0800 Subject: [PATCH 1/2] feat: add open library page --- components/Navigator/MainNavigator.tsx | 1 + pages/library/index.module.less | 175 ++++++++++++++ pages/library/index.tsx | 312 +++++++++++++++++++++++++ translation/en-US.ts | 70 ++++++ translation/zh-CN.ts | 59 +++++ translation/zh-TW.ts | 59 +++++ 6 files changed, 676 insertions(+) create mode 100644 pages/library/index.module.less create mode 100644 pages/library/index.tsx diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 943f739..49ca57f 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -41,6 +41,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ { href: '/project', title: t('self_developed_projects') }, { href: '/search/project', title: t('bazaar_projects') }, { href: '/issue', title: 'GitHub issues' }, + { href: '/library', title: t('open_library') }, { href: '/license-filter', title: t('license_filter') }, { href: '/finance', title: t('finance_page_title') }, ], diff --git a/pages/library/index.module.less b/pages/library/index.module.less new file mode 100644 index 0000000..0bb375d --- /dev/null +++ b/pages/library/index.module.less @@ -0,0 +1,175 @@ +.libraryPage { + padding-bottom: 4rem; +} + +.hero { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 28px; + background: + radial-gradient(circle at top right, rgba(25, 135, 84, 0.16), transparent 34%), + linear-gradient(135deg, rgba(13, 110, 253, 0.08), rgba(255, 193, 7, 0.12)); + padding: clamp(2rem, 6vw, 4rem); +} + +.heroMeta { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin: 0; + padding: 0; + list-style: none; +} + +.heroMetaItem { + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08); + border: 1px solid rgba(15, 23, 42, 0.06); + border-radius: 18px; + background: rgba(255, 255, 255, 0.86); + padding: 1.25rem; +} + +.sectionCard { + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.07); + border: 1px solid rgba(15, 23, 42, 0.06); + border-radius: 24px; + background: #fff; + padding: clamp(1.25rem, 4vw, 2rem); +} + +.bookCard { + transition: + transform 0.2s ease, + box-shadow 0.2s ease; + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 18px; + height: 100%; + + &:hover { + transform: translateY(-3px); + box-shadow: 0 18px 40px rgba(15, 23, 42, 0.1); + } +} + +.activeBookCard { + box-shadow: 0 18px 40px rgba(13, 110, 253, 0.12); + border-color: rgba(13, 110, 253, 0.42); +} + +.bookCover { + display: flex; + justify-content: center; + align-items: center; + border-radius: 16px; + background: linear-gradient(135deg, rgba(13, 110, 253, 0.12), rgba(25, 135, 84, 0.12)); + aspect-ratio: 4 / 3; + color: #0d6efd; + font-weight: 700; + font-size: 2.5rem; +} + +.statusAvailable { + background: rgba(25, 135, 84, 0.1) !important; + color: #198754 !important; +} + +.statusBorrowed { + background: rgba(220, 53, 69, 0.1) !important; + color: #dc3545 !important; +} + +.catalogTable { + min-width: 720px; +} + +@media (width <= 576px) { + .catalogTable { + min-width: 0; + + thead { + display: none; + } + + tbody, + tr, + td { + display: block; + width: 100%; + } + + tr { + border: 1px solid rgba(15, 23, 42, 0.08); + border-radius: 14px; + padding: 0.65rem 0.85rem; + + & + tr { + margin-top: 0.85rem; + } + } + + td { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + border: 0; + padding: 0.35rem 0; + text-align: right; + + &::before { + flex: 0 0 5rem; + content: attr(data-label); + color: #6c757d; + font-weight: 600; + text-align: left; + } + } + } +} + +.guideList { + counter-reset: guide-step; +} + +.guideStep { + position: relative; + counter-increment: guide-step; + border-left: 3px solid rgba(13, 110, 253, 0.22); + padding: 0 0 1.5rem 1.5rem; + + &:last-child { + border-left-color: transparent; + padding-bottom: 0; + } + + &::before { + display: inline-flex; + position: absolute; + top: 0; + left: -0.85rem; + justify-content: center; + align-items: center; + border-radius: 999px; + background: #0d6efd; + width: 1.6rem; + height: 1.6rem; + content: counter(guide-step); + color: #fff; + font-weight: 700; + font-size: 0.85rem; + } +} + +.detailPanel { + border-radius: 20px; + background: rgba(248, 249, 250, 0.85); + padding: 1.5rem; + height: 100%; +} + +.borrowPanel { + border: 1px dashed rgba(13, 110, 253, 0.32); + border-radius: 20px; + background: rgba(13, 110, 253, 0.06); + padding: 1.5rem; + height: 100%; +} diff --git a/pages/library/index.tsx b/pages/library/index.tsx new file mode 100644 index 0000000..b6d84e9 --- /dev/null +++ b/pages/library/index.tsx @@ -0,0 +1,312 @@ +import { observer } from 'mobx-react'; +import { FC, useContext, useMemo, useState } from 'react'; +import { Badge, Button, Card, Col, Container, Form, Row, Table } from 'react-bootstrap'; + +import { PageHead } from '../../components/Layout/PageHead'; +import { I18nContext, I18nKey } from '../../models/Translation'; +import styles from './index.module.less'; + +type BookStatus = 'available' | 'borrowed'; + +type CategoryFilter = I18nKey | 'all'; + +interface LibraryBook { + author: string; + categoryKey: I18nKey; + code: string; + descriptionKey: I18nKey; + dueDate?: string; + locationKey: I18nKey; + status: BookStatus; + title: string; +} + +const books: LibraryBook[] = [ + { + code: 'OSB-001', + title: 'Working in Public', + author: 'Nadia Eghbal', + categoryKey: 'library_category_open_source', + status: 'available', + locationKey: 'library_location_shelf_a', + descriptionKey: 'library_book_working_public_desc', + }, + { + code: 'OSB-002', + title: 'The Cathedral & the Bazaar', + author: 'Eric S. Raymond', + categoryKey: 'library_category_community', + status: 'borrowed', + dueDate: '2026-07-12', + locationKey: 'library_location_checked_out', + descriptionKey: 'library_book_cathedral_desc', + }, + { + code: 'OSB-003', + title: 'Producing Open Source Software', + author: 'Karl Fogel', + categoryKey: 'library_category_governance', + status: 'available', + locationKey: 'library_location_shelf_b', + descriptionKey: 'library_book_producing_desc', + }, + { + code: 'OSB-004', + title: 'Open Source for Business', + author: 'Heather Meeker', + categoryKey: 'library_category_legal', + status: 'available', + locationKey: 'library_location_shelf_c', + descriptionKey: 'library_book_business_desc', + }, +]; + +const guideKeys: [I18nKey, I18nKey][] = [ + ['library_guide_step_1_title', 'library_guide_step_1_desc'], + ['library_guide_step_2_title', 'library_guide_step_2_desc'], + ['library_guide_step_3_title', 'library_guide_step_3_desc'], +]; + +const LibraryPage: FC = observer(() => { + const { t } = useContext(I18nContext); + const [keyword, setKeyword] = useState(''); + const [category, setCategory] = useState('all'); + const [status, setStatus] = useState('all'); + const [selectedCode, setSelectedCode] = useState(books[0].code); + const availableCount = books.filter(({ status }) => status === 'available').length; + const categoryKeys = useMemo(() => [...new Set(books.map(({ categoryKey }) => categoryKey))], []); + const normalizedKeyword = keyword.trim().toLowerCase(); + const filteredBooks = books.filter(book => { + const translatedCategory = t(book.categoryKey).toLowerCase(); + + return ( + (category === 'all' || book.categoryKey === category) && + (status === 'all' || book.status === status) && + (!normalizedKeyword || + [book.code, book.title, book.author, translatedCategory].some(item => + item.toLowerCase().includes(normalizedKeyword), + )) + ); + }); + const activeBook = + filteredBooks.find(({ code }) => code === selectedCode) || filteredBooks[0] || books[0]; + + const statusText = (status: BookStatus) => + status === 'available' ? t('library_status_available') : t('library_status_borrowed'); + + const statusClass = (status: BookStatus) => + status === 'available' ? styles.statusAvailable : styles.statusBorrowed; + + return ( + + + + + + + {t('library_badge')} + +

{t('library_hero_title')}

+

{t('library_hero_intro')}

+
+ + +
+ + +
    +
  • +

    {t('library_stat_total')}

    + {books.length} +
  • +
  • +

    {t('library_stat_available')}

    + {availableCount} +
  • +
  • +

    {t('library_stat_process')}

    + 3 +
  • +
+ +
+ +
+
+
+

{t('library_browse_title')}

+

{t('library_browse_intro')}

+
+ + + setKeyword(currentTarget.value)} + /> + + + setCategory(currentTarget.value as CategoryFilter)} + > + + {categoryKeys.map(item => ( + + ))} + + + + + setStatus(currentTarget.value as BookStatus | 'all') + } + > + + + + + + +
+ + {filteredBooks.map(({ code, title, author, categoryKey, status, descriptionKey }) => ( + + +
{code.split('-')[1]}
+
+ + {t(categoryKey)} + + {statusText(status)} +
+ + {title} + +

{author}

+ {t(descriptionKey)} + +
+ + ))} +
+ {!filteredBooks[0] &&

{t('library_empty_result')}

} +
+ +
+
+

{t('library_catalog_title')}

+

{t('library_catalog_intro')}

+
+ + + + + + + + + + + + {books.map(({ code, title, author, status, locationKey, dueDate }) => ( + + + + + + + + ))} + +
{t('library_table_code')}{t('library_table_book')}{t('library_table_status')}{t('library_table_location')}{t('library_table_due')}
+ {code} + + {title} + {author} + + {statusText(status)} + {t(locationKey)} + {dueDate || t('library_due_not_applicable')} +
+
+ + + +
+

{t('library_guide_title')}

+
    + {guideKeys.map(([titleKey, descKey]) => ( +
  1. +

    {t(titleKey)}

    +

    {t(descKey)}

    +
  2. + ))} +
+
+ + +
+

{t('library_detail_title')}

+ + +
+
+ {activeBook.code} + + {statusText(activeBook.status)} + +
+

{activeBook.title}

+

{activeBook.author}

+

{t(activeBook.descriptionKey)}

+

+ {t('library_detail_location_prefix')} + {t(activeBook.locationKey)} +

+

+ {t('library_detail_due_prefix')} + {activeBook.dueDate || t('library_due_not_applicable')} +

+
+ + + + +
+
+ +
+
+ ); +}); + +export default LibraryPage; diff --git a/translation/en-US.ts b/translation/en-US.ts index 4419cb6..6f5600d 100644 --- a/translation/en-US.ts +++ b/translation/en-US.ts @@ -12,6 +12,7 @@ export default { self_developed_projects: 'Self-developed projects', bazaar_projects: 'Bazaar projects', open_source_bazaar: 'Open Source Bazaar', + open_library: 'Open Library', home_page: 'Home Page', wiki: 'Wiki', @@ -210,6 +211,75 @@ export default { 'Find corresponding ETFs or index funds, paying attention to fees and fund size.', finance_edu_next_3: 'Set up a recurring investment plan and track valuation and drawdown.', + // Open Library + library_page_title: 'Open Library', + library_page_description: + 'Browse the Open Source Bazaar library catalog, borrowing status, and borrowing guide.', + library_badge: 'Open Library', + library_hero_title: 'Open Library', + library_hero_intro: + 'A curated shelf for open-source governance, community collaboration, legal compliance, and project operations so contributors can find the right reading material faster.', + library_hero_cta_catalog: 'View catalog', + library_hero_cta_guide: 'Borrowing guide', + library_stat_total: 'Books', + library_stat_available: 'Available', + library_stat_process: 'Steps', + library_browse_title: 'Browse books', + library_browse_intro: + 'Explore the collection by topic, check status first, then decide whether to borrow.', + library_filter_keyword: 'Search by title, author, or ID', + library_filter_category: 'Filter by category', + library_filter_status: 'Filter by status', + library_filter_all_categories: 'All categories', + library_filter_all_statuses: 'All statuses', + library_category_open_source: 'Open-source culture', + library_category_community: 'Community collaboration', + library_category_governance: 'Project governance', + library_category_legal: 'Legal compliance', + library_view_detail: 'View details', + library_empty_result: 'No matching books found. Try another keyword or filter.', + library_catalog_title: 'Catalog and status', + library_catalog_intro: + 'The catalog lists book ID, title, current status, location, and expected return date.', + library_table_code: 'ID', + library_table_book: 'Book', + library_table_status: 'Status', + library_table_location: 'Location', + library_table_due: 'Due', + library_status_available: 'Available', + library_status_borrowed: 'Borrowed', + library_location_shelf_a: 'Shelf A · Open-source culture', + library_location_shelf_b: 'Shelf B · Community governance', + library_location_shelf_c: 'Shelf C · Legal compliance', + library_location_checked_out: 'Checked out', + library_due_not_applicable: 'N/A', + library_guide_title: 'Borrowing guide', + library_guide_step_1_title: 'Check availability', + library_guide_step_1_desc: + 'Use the catalog to confirm whether the book is available and note its book ID.', + library_guide_step_2_title: 'Submit borrowing info', + library_guide_step_2_desc: + 'Submit your name, contact, book ID, and expected borrowing period through the community channel.', + library_guide_step_3_title: 'Return on time', + library_guide_step_3_desc: + 'Return the book after reading and optionally leave a short note for the next reader.', + library_detail_title: 'Book details', + library_detail_location_prefix: 'Current location: ', + library_detail_due_prefix: 'Expected return: ', + library_borrow_info_title: 'Borrowing info', + library_borrow_info_desc: + 'This version shows borrowing status and workflow first. It can later connect to a form or community channel.', + library_borrow_available_action: 'Currently available', + library_borrow_wait_action: 'Currently borrowed', + library_book_working_public_desc: + 'A good entry point for understanding open-source maintainers, contributors, and community sustainability.', + library_book_cathedral_desc: + 'A classic open-source culture text for understanding the early thinking behind open collaboration.', + library_book_producing_desc: + 'Focuses on open-source project management, contribution workflow, release cadence, and governance.', + library_book_business_desc: + 'Explains open-source licensing, compliance, and adoption strategy from business and legal angles.', + // Hackathon hackathon_detail: 'Hackathon Details', hackathon_highlights: 'Tracks', diff --git a/translation/zh-CN.ts b/translation/zh-CN.ts index ae27758..ae35d41 100644 --- a/translation/zh-CN.ts +++ b/translation/zh-CN.ts @@ -9,6 +9,7 @@ export default { open_source_projects: '开源项目', self_developed_projects: '自研项目', bazaar_projects: '市集项目', + open_library: '开源图书馆', activity: '活动', hackathon: '黑客马拉松', bounty: '开源悬赏', @@ -206,6 +207,64 @@ export default { finance_edu_next_2: '了解对应的 ETF 或联接基金,关注费率与规模。', finance_edu_next_3: '设定定投计划并持续跟踪估值、回撤。', + // Open Library + library_page_title: '开源图书馆', + library_page_description: '浏览开源市集图书目录、借阅状态与借阅指南。', + library_badge: 'Open Library', + library_hero_title: '开源图书馆', + library_hero_intro: + '把开源治理、社区协作、法律合规和项目运营相关书籍集中整理,让贡献者快速找到适合当前阶段的阅读材料。', + library_hero_cta_catalog: '查看目录', + library_hero_cta_guide: '借阅指南', + library_stat_total: '馆藏图书', + library_stat_available: '可借图书', + library_stat_process: '借阅步骤', + library_browse_title: '图书浏览', + library_browse_intro: '按主题快速了解馆藏内容,先看状态,再决定是否借阅。', + library_filter_keyword: '搜索书名、作者或编号', + library_filter_category: '按主题筛选', + library_filter_status: '按状态筛选', + library_filter_all_categories: '全部主题', + library_filter_all_statuses: '全部状态', + library_category_open_source: '开源文化', + library_category_community: '社区协作', + library_category_governance: '项目治理', + library_category_legal: '法律合规', + library_view_detail: '查看详情', + library_empty_result: '没有找到匹配的图书,可以调整关键词或筛选条件。', + library_catalog_title: '目录与状态', + library_catalog_intro: '目录展示编号、图书、当前状态、存放位置和预计归还时间。', + library_table_code: '编号', + library_table_book: '图书', + library_table_status: '状态', + library_table_location: '位置', + library_table_due: '预计归还', + library_status_available: '可借', + library_status_borrowed: '已借出', + library_location_shelf_a: 'A 架 · 开源文化', + library_location_shelf_b: 'B 架 · 社区治理', + library_location_shelf_c: 'C 架 · 法律合规', + library_location_checked_out: '读者借阅中', + library_due_not_applicable: '无', + library_guide_title: '借阅指南', + library_guide_step_1_title: '确认状态', + library_guide_step_1_desc: '先在目录中查看图书是否可借,并记录图书编号。', + library_guide_step_2_title: '提交借阅信息', + library_guide_step_2_desc: '通过社区协作渠道提交姓名、联系方式、图书编号和预计借阅时长。', + library_guide_step_3_title: '按期归还', + library_guide_step_3_desc: '阅读结束后归还图书,并可补充短评帮助下一位读者判断是否适合借阅。', + library_detail_title: '图书详情', + library_detail_location_prefix: '当前位置:', + library_detail_due_prefix: '预计归还:', + library_borrow_info_title: '借阅信息', + library_borrow_info_desc: '当前版本先展示借阅状态与流程,后续可接入表单或社区协作渠道。', + library_borrow_available_action: '当前可借', + library_borrow_wait_action: '当前借出', + library_book_working_public_desc: '适合理解开源维护者、贡献者关系和社区可持续性的入门读物。', + library_book_cathedral_desc: '经典开源文化文本,帮助读者理解开放协作的早期思想脉络。', + library_book_producing_desc: '聚焦开源项目管理、贡献流程、发布节奏和社区治理方法。', + library_book_business_desc: '从商业和法律角度理解开源许可、合规和企业采用策略。', + // Hackathon hackathon_detail: '黑客松详情', hackathon_highlights: '赛道方向', diff --git a/translation/zh-TW.ts b/translation/zh-TW.ts index a657e06..365155b 100644 --- a/translation/zh-TW.ts +++ b/translation/zh-TW.ts @@ -9,6 +9,7 @@ export default { open_source_projects: '開源項目', self_developed_projects: '自研項目', bazaar_projects: '市集項目', + open_library: '開源圖書館', activity: '活動', hackathon: '黑客馬拉松', bounty: '開源懸賞', @@ -206,6 +207,64 @@ export default { finance_edu_next_2: '了解對應的 ETF 或聯接基金,關注費率與規模。', finance_edu_next_3: '設定定投計畫並持續跟蹤估值、回撤。', + // Open Library + library_page_title: '開源圖書館', + library_page_description: '瀏覽開源市集圖書目錄、借閱狀態與借閱指南。', + library_badge: 'Open Library', + library_hero_title: '開源圖書館', + library_hero_intro: + '把開源治理、社群協作、法律合規和項目運營相關書籍集中整理,讓貢獻者快速找到適合當前階段的閱讀材料。', + library_hero_cta_catalog: '查看目錄', + library_hero_cta_guide: '借閱指南', + library_stat_total: '館藏圖書', + library_stat_available: '可借圖書', + library_stat_process: '借閱步驟', + library_browse_title: '圖書瀏覽', + library_browse_intro: '按主題快速了解館藏內容,先看狀態,再決定是否借閱。', + library_filter_keyword: '搜尋書名、作者或編號', + library_filter_category: '按主題篩選', + library_filter_status: '按狀態篩選', + library_filter_all_categories: '全部主題', + library_filter_all_statuses: '全部狀態', + library_category_open_source: '開源文化', + library_category_community: '社群協作', + library_category_governance: '項目治理', + library_category_legal: '法律合規', + library_view_detail: '查看詳情', + library_empty_result: '沒有找到匹配的圖書,可以調整關鍵詞或篩選條件。', + library_catalog_title: '目錄與狀態', + library_catalog_intro: '目錄展示編號、圖書、當前狀態、存放位置和預計歸還時間。', + library_table_code: '編號', + library_table_book: '圖書', + library_table_status: '狀態', + library_table_location: '位置', + library_table_due: '預計歸還', + library_status_available: '可借', + library_status_borrowed: '已借出', + library_location_shelf_a: 'A 架 · 開源文化', + library_location_shelf_b: 'B 架 · 社群治理', + library_location_shelf_c: 'C 架 · 法律合規', + library_location_checked_out: '讀者借閱中', + library_due_not_applicable: '無', + library_guide_title: '借閱指南', + library_guide_step_1_title: '確認狀態', + library_guide_step_1_desc: '先在目錄中查看圖書是否可借,並記錄圖書編號。', + library_guide_step_2_title: '提交借閱信息', + library_guide_step_2_desc: '通過社群協作渠道提交姓名、聯繫方式、圖書編號和預計借閱時長。', + library_guide_step_3_title: '按期歸還', + library_guide_step_3_desc: '閱讀結束後歸還圖書,並可補充短評幫助下一位讀者判斷是否適合借閱。', + library_detail_title: '圖書詳情', + library_detail_location_prefix: '當前位置:', + library_detail_due_prefix: '預計歸還:', + library_borrow_info_title: '借閱信息', + library_borrow_info_desc: '當前版本先展示借閱狀態與流程,後續可接入表單或社群協作渠道。', + library_borrow_available_action: '當前可借', + library_borrow_wait_action: '當前借出', + library_book_working_public_desc: '適合理解開源維護者、貢獻者關係和社群可持續性的入門讀物。', + library_book_cathedral_desc: '經典開源文化文本,幫助讀者理解開放協作的早期思想脈絡。', + library_book_producing_desc: '聚焦開源項目管理、貢獻流程、發布節奏和社群治理方法。', + library_book_business_desc: '從商業和法律角度理解開源許可、合規和企業採用策略。', + // Hackathon hackathon_detail: '黑客松詳情', hackathon_highlights: '賽道方向', From 686b1f432ed8e9981a025861badf2107b8216abe Mon Sep 17 00:00:00 2001 From: jordansilly77-stack Date: Mon, 22 Jun 2026 09:11:20 +0800 Subject: [PATCH 2/2] chore: polish library status styles --- pages/library/index.module.less | 11 ++++++----- pages/library/index.tsx | 10 +++++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pages/library/index.module.less b/pages/library/index.module.less index 0bb375d..fe3cc56 100644 --- a/pages/library/index.module.less +++ b/pages/library/index.module.less @@ -68,13 +68,13 @@ } .statusAvailable { - background: rgba(25, 135, 84, 0.1) !important; - color: #198754 !important; + background: rgba(25, 135, 84, 0.1); + color: #198754; } .statusBorrowed { - background: rgba(220, 53, 69, 0.1) !important; - color: #dc3545 !important; + background: rgba(220, 53, 69, 0.1); + color: #dc3545; } .catalogTable { @@ -116,7 +116,8 @@ text-align: right; &::before { - flex: 0 0 5rem; + flex: 0 0 auto; + min-width: 5rem; content: attr(data-label); color: #6c757d; font-weight: 600; diff --git a/pages/library/index.tsx b/pages/library/index.tsx index b6d84e9..0470001 100644 --- a/pages/library/index.tsx +++ b/pages/library/index.tsx @@ -191,7 +191,9 @@ const LibraryPage: FC = observer(() => { {t(categoryKey)} - {statusText(status)} + + {statusText(status)} + {title} @@ -239,7 +241,9 @@ const LibraryPage: FC = observer(() => { {author} - {statusText(status)} + + {statusText(status)} + {t(locationKey)} @@ -273,7 +277,7 @@ const LibraryPage: FC = observer(() => {
{activeBook.code} - + {statusText(activeBook.status)}