-
Notifications
You must be signed in to change notification settings - Fork 12
Feature/award page refactor #81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
57e6c65
b05ec5c
78ef8aa
4f6c12a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,172 @@ | ||
| import { TableCellValue } from 'mobx-lark'; | ||
| import { observer } from 'mobx-react'; | ||
| import { cache, compose, errorLogger } from 'next-ssr-middleware'; | ||
| import { FC } from 'react'; | ||
| import { FC, useContext } from 'react'; | ||
| import { Badge, Card, Col, Container, Row } from 'react-bootstrap'; | ||
| const ALLOWED_EMBED_HOSTS = new Set([ | ||
| 'player.bilibili.com', | ||
| 'www.youtube.com', | ||
| 'www.youtube-nocookie.com', | ||
| ]); | ||
|
|
||
| const getSafeEmbedUrl = (value: unknown): string | null => { | ||
| if (typeof value !== 'string' || !value) return null; | ||
| try { | ||
| const url = new URL(value); | ||
| return ALLOWED_EMBED_HOSTS.has(url.hostname) ? url.toString() : null; | ||
| } catch { return null; } | ||
| }; | ||
|
|
||
| import { PageHead } from '../../components/Layout/PageHead'; | ||
| import { SectionTitle } from '../../components/Layout/SectionTitle'; | ||
| import { Award, AwardModel } from '../../models/Award'; | ||
| import { I18nContext } from '../../models/Translation'; | ||
|
|
||
| export const getServerSideProps = compose(cache(), errorLogger, async () => { | ||
| const awards = await new AwardModel().getAll(); | ||
|
|
||
| return { props: { awards } }; | ||
| }); | ||
|
|
||
| const AwardPage: FC<{ awards: Award[] }> = ({ awards }) => { | ||
| return <></>; | ||
| }; | ||
| const NominationCard: FC<{ award: Award; index: number }> = observer(({ award, index }) => { | ||
| const { t } = useContext(I18nContext); | ||
| const name = (award.nomineeName ?? award.awardName ?? '') as string; | ||
| const desc = (award.nomineeDesc ?? award.reason ?? '') as string; | ||
| const nominator = (award.nominator ?? '') as string; | ||
| const videoUrl = getSafeEmbedUrl(award.videoUrl) ?? ''; | ||
| const votes = (award.votes ?? 0) as number; | ||
|
|
||
| return ( | ||
| <Card className="h-100 shadow-sm border-0"> | ||
| {videoUrl && ( | ||
| <div className="ratio ratio-16x9"> | ||
| <iframe | ||
| src=loading="lazy" {videoUrl} | ||
| title={name} | ||
| allowFullScreen | ||
| className="rounded-top" | ||
| /> | ||
| </div> | ||
| )} | ||
| <Card.Body> | ||
| <div className="d-flex justify-content-between align-items-start mb-2"> | ||
| <Badge bg="primary" className="me-2">#{index + 1}</Badge> | ||
| {votes > 0 && ( | ||
| <Badge bg="warning" text="dark"> | ||
| {votes} {t('votes')} | ||
| </Badge> | ||
| )} | ||
| </div> | ||
| <Card.Title className="fs-5">{name}</Card.Title> | ||
| {desc && ( | ||
| <Card.Text className="text-muted small mt-2" style={{ maxHeight: 80, overflow: 'hidden' }}> | ||
| {desc} | ||
| </Card.Text> | ||
| )} | ||
| {nominator && ( | ||
| <div className="mt-2 small"> | ||
| <span className="text-muted">{t('nominated_by')}: </span> | ||
| <span className="fw-medium">{nominator}</span> | ||
| </div> | ||
| )} | ||
| </Card.Body> | ||
| </Card> | ||
| ); | ||
| }); | ||
|
|
||
| const AwardPage: FC<{ awards: Award[] }> = observer(({ awards }) => { | ||
| const { t } = useContext(I18nContext); | ||
|
|
||
| return ( | ||
| <Container className="py-4"> | ||
| <PageHead | ||
| title={t('open_collaborator_award')} | ||
| description={t('award_page_description')} | ||
| /> | ||
|
|
||
| {/* Hero Section */} | ||
| <section className="text-center py-5 mb-4 bg-light rounded-3"> | ||
| <h1 className="display-4 fw-bold mb-3">{t('open_collaborator_award')}</h1> | ||
| <p className="lead text-muted mb-4">{t('award_page_description')}</p> | ||
| <div className="d-flex justify-content-center gap-3 flex-wrap"> | ||
| <a href="/article/open-collaborator-award" className="btn btn-outline-primary"> | ||
| {t('learn_more')} | ||
| </a> | ||
| <a href="#nominations" className="btn btn-primary"> | ||
| {t('view_nominations')} | ||
| </a> | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* Video Intro */} | ||
| <section className="mb-5"> | ||
| <SectionTitle>{t('award_intro_video')}</SectionTitle> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 这里会渲染出
🤖 Prompt for AI Agents |
||
| <div className="ratio ratio-16x9 border rounded overflow-hidden shadow-sm"> | ||
| <iframe | ||
| src=loading="lazy" "//player.bilibili.com/player.html?aid=978564817&bvid=BV1c44y1x7ij&cid=494424932&page=1&high_quality=1&danmaku=0" | ||
| title={t('open_collaborator_award') as string} | ||
| scrolling="no" | ||
| frameBorder="0" | ||
| allowFullScreen | ||
| /> | ||
| </div> | ||
| </section> | ||
|
|
||
| {/* Awards List */} | ||
| <section id="nominations" className="mb-5"> | ||
| <SectionTitle count={awards.length}>{t('award_nominations')}</SectionTitle> | ||
| {awards.length > 0 ? ( | ||
| <Row className="g-4" xs={1} sm={2} lg={3}> | ||
| {awards.map((award, index) => ( | ||
| <Col key={index}> | ||
| <NominationCard award={award} index={index} /> | ||
| </Col> | ||
| ))} | ||
| </Row> | ||
| ) : ( | ||
| <div className="text-center py-5 text-muted"> | ||
| <p className="fs-5">{t('no_nominations_yet')}</p> | ||
| <p>{t('nomination_coming_soon')}</p> | ||
| </div> | ||
| )} | ||
| </section> | ||
|
|
||
| {/* How to Nominate */} | ||
| <section className="mb-5 bg-light rounded-3 p-4 p-md-5"> | ||
| <h3 className="mb-4">{t('how_to_nominate')}</h3> | ||
| <Row className="g-4"> | ||
| {[1, 2, 3, 4].map((step) => ( | ||
| <Col key={step} md={3} sm={6}> | ||
| <div className="text-center"> | ||
| <div | ||
| className="d-inline-flex justify-content-center align-items-center rounded-circle bg-primary text-white mb-3" | ||
| style={{ width: 48, height: 48, fontSize: 20 }} | ||
| > | ||
| {step} | ||
| </div> | ||
| <h5>{t('nomination_step' + step + '_title' as any)}</h5> | ||
| <p className="text-muted small">{t('nomination_step' + step + '_desc' as any)}</p> | ||
| </div> | ||
| </Col> | ||
| ))} | ||
| </Row> | ||
| </section> | ||
|
|
||
| {/* Award Process / About */} | ||
| <section className="mb-5"> | ||
| <h3 className="mb-4">{t('about_award')}</h3> | ||
| <Card className="border-0 shadow-sm"> | ||
| <Card.Body className="p-4"> | ||
| <p>{t('award_about_text')}</p> | ||
| <div className="d-flex gap-3 mt-4"> | ||
| <a href="/article/open-collaborator-award" className="btn btn-outline-primary"> | ||
| {t('read_full_details')} | ||
| </a> | ||
| </div> | ||
| </Card.Body> | ||
| </Card> | ||
| </section> | ||
| </Container> | ||
| ); | ||
| }); | ||
|
|
||
| export default AwardPage; | ||
| export default AwardPage; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { observer } from "mobx-react"; | ||
| import { cache, compose, errorLogger } from "next-ssr-middleware"; | ||
| import { FC, useContext } from "react"; | ||
| import { Badge, Card, Col, Container, Row } from "react-bootstrap"; | ||
|
|
||
| import { PageHead } from "../../../components/Layout/PageHead"; | ||
| import { I18nContext } from "../../../models/Translation"; | ||
| import { libraryBooks } from "../books"; | ||
|
|
||
| export const getServerSideProps = compose(cache(), errorLogger, async ({ params }) => { | ||
| const book = libraryBooks.find((b) => b.id === params.id); | ||
| if (!book) return { notFound: true }; | ||
| return { props: { book } }; | ||
| }); | ||
|
Comment on lines
+10
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 确认 next-ssr-middleware compose 回调中 params 的类型定义
fd -t f 'index.d.ts|index.ts' node_modules/next-ssr-middleware 2>/dev/null | xargs -r rg -n 'params' -C2 || echo "查看 next-ssr-middleware 类型以确认 params 是否可能为 undefined"Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io Length of output: 185 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) 定位相关页面与 compose 用法
git ls-files 'pages/library/[id].tsx' 'pages/**/*.tsx' 'package.json' 'tsconfig.json' | sed -n '1,120p'
echo '--- pages/library/[id].tsx ---'
wc -l pages/library/[id].tsx
cat -n pages/library/[id].tsx | sed -n '1,180p'
echo '--- search compose/cache/errorLogger/getServerSideProps ---'
rg -n "compose\\(|cache\\(|errorLogger|getServerSideProps|params\\.id|notFound" pages components lib . -g '!node_modules' -g '!dist' -g '!build'
# 2) 若存在相关类型/实现,查看定义
echo '--- possible middleware package references ---'
rg -n "next-ssr-middleware|compose\\s*\\(" . -g '!node_modules' -g '!dist' -g '!build'
echo '--- package.json ---'
cat -n package.json | sed -n '1,220p'
echo '--- tsconfig.json ---'
cat -n tsconfig.json | sed -n '1,220p'Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io Length of output: 20949 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 3) 精准检查该页面的动态路由数据获取写法
ast-grep outline pages/library/[id].tsx --view expanded || trueRepository: Open-Source-Bazaar/Open-Source-Bazaar.github.io Length of output: 391 🌐 Web query:
💡 Result: In Next.js, the Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
# 查看 libraryBooks 的 id 类型,判断 params.id 的比较是否存在实际类型不匹配风险
ast-grep outline pages/library/books.ts --view expanded || true
echo '--- pages/library/books.ts ---'
wc -l pages/library/books.ts
cat -n pages/library/books.ts | sed -n '1,220p'Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io Length of output: 6429 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 仅读取与 book 数据结构相关的定义
rg -n "export const libraryBooks|id:|interface .*Book|type .*Book" pages/library -g '!node_modules'Repository: Open-Source-Bazaar/Open-Source-Bazaar.github.io Length of output: 1099 为 🤖 Prompt for AI Agents |
||
|
|
||
| const BookDetailPage: FC<{ book: (typeof libraryBooks)[number] }> = observer(({ book }) => { | ||
| const { t } = useContext(I18nContext); | ||
|
|
||
| return ( | ||
| <Container className="py-4"> | ||
| <PageHead title={book.title} description={book.description} /> | ||
| <a href="/library" className="btn btn-sm btn-outline-secondary mb-4"> | ||
| ← {t("back_to_library")} | ||
| </a> | ||
|
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 封面图与返回链接应使用 React Bootstrap 组件 / 第 28-33 行的原生 As per coding guidelines: "ALWAYS use React Bootstrap components instead of custom HTML elements in UI code". Also applies to: 28-33, 105-105 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| <Row> | ||
| <Col md={4} className="text-center mb-4"> | ||
| <img | ||
| src={book.cover} | ||
| alt={book.title} | ||
| className="rounded shadow" | ||
| style={{ maxWidth: "100%", maxHeight: 400, objectFit: "cover" }} | ||
| /> | ||
| <div className="mt-3"> | ||
| <Badge bg={book.status === "available" ? "success" : "warning"} className="fs-6 px-3 py-2"> | ||
| {t(book.status === "available" ? "library_status_available" : "library_status_borrowed" as any)} | ||
| </Badge> | ||
| </div> | ||
| </Col> | ||
|
|
||
| <Col md={8}> | ||
| <h2>{book.title}</h2> | ||
| <p className="text-muted fs-5">{book.author}</p> | ||
|
|
||
| {book.tags && ( | ||
| <div className="mb-3 d-flex gap-2 flex-wrap"> | ||
| {book.tags.map((tag) => ( | ||
| <Badge key={tag} bg="info" className="bg-opacity-25 text-dark">{tag}</Badge> | ||
| ))} | ||
| </div> | ||
| )} | ||
|
|
||
| <p className="lead">{book.description}</p> | ||
|
|
||
| <Card className="mb-4"> | ||
| <Card.Header>{t("book_details")}</Card.Header> | ||
| <Card.Body> | ||
| <Row className="g-2"> | ||
| {book.publisher && ( | ||
| <Col xs={6}><small className="text-muted">{t("publisher")}</small><div>{book.publisher}</div></Col> | ||
| )} | ||
| {book.year && ( | ||
| <Col xs={6}><small className="text-muted">{t("year")}</small><div>{book.year}</div></Col> | ||
| )} | ||
| {book.isbn && ( | ||
| <Col xs={6}><small className="text-muted">ISBN</small><div>{book.isbn}</div></Col> | ||
| )} | ||
| {book.pages && ( | ||
| <Col xs={6}><small className="text-muted">{t("pages")}</small><div>{book.pages}</div></Col> | ||
| )} | ||
| {book.language && ( | ||
| <Col xs={6}><small className="text-muted">{t("language")}</small><div>{book.language}</div></Col> | ||
| )} | ||
| <Col xs={6}><small className="text-muted">{t("category")}</small><div>{t("library_category_" + book.category as any)}</div></Col> | ||
| </Row> | ||
| </Card.Body> | ||
| </Card> | ||
|
|
||
| {book.status === "borrowed" && book.borrower && ( | ||
| <Card className="mb-4 border-warning"> | ||
| <Card.Header className="bg-warning bg-opacity-10">{t("borrowing_info")}</Card.Header> | ||
| <Card.Body> | ||
| <Row> | ||
| <Col xs={6}><small className="text-muted">{t("borrower")}</small><div className="fw-bold">{book.borrower}</div></Col> | ||
| {book.borrowDate && <Col xs={6}><small className="text-muted">{t("borrow_date")}</small><div>{book.borrowDate}</div></Col>} | ||
| {book.returnDate && <Col xs={6} className="mt-2"><small className="text-muted">{t("expected_return_date")}</small><div>{book.returnDate}</div></Col>} | ||
| </Row> | ||
| </Card.Body> | ||
| </Card> | ||
| )} | ||
|
|
||
| <Card className="mb-4"> | ||
| <Card.Header>{t("borrowing_guide")}</Card.Header> | ||
| <Card.Body> | ||
| <ol className="mb-0"> | ||
| <li className="mb-2">{t("borrowing_guide_step1")}</li> | ||
| <li className="mb-2">{t("borrowing_guide_step2")}</li> | ||
| <li className="mb-2">{t("borrowing_guide_step3")}</li> | ||
| <li className="mb-2">{t("borrowing_guide_step4")}</li> | ||
| <li>{t("borrowing_guide_step5")}</li> | ||
| </ol> | ||
| </Card.Body> | ||
| </Card> | ||
|
|
||
| <a href="/library" className="btn btn-primary">← {t("back_to_library")}</a> | ||
| </Col> | ||
| </Row> | ||
| </Container> | ||
| ); | ||
| }); | ||
|
|
||
| export default BookDetailPage; | ||
Uh oh!
There was an error while loading. Please reload this page.