Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
npm test
pnpm test
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1 @@
npm run build
pnpm run build
2 changes: 1 addition & 1 deletion components/Layout/CardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Col, Pagination, Row } from 'react-bootstrap';
import { SearchPageMeta } from '../../models/System';

export interface CardPageProps extends SearchPageMeta {
Card: ComponentClass<any> | FC<any>;
Card: ComponentClass<Record<string, unknown>> | FC<Record<string, unknown>>;
cardLinkOf?: (id: string) => string;
pageLinkOf: (page: number) => string;
}
Expand Down
12 changes: 12 additions & 0 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
name: t('hackathon'),
},
{
href: '/open-library',
name: t('open_library'),
},
];

export interface MainNavigatorProps {
Expand All @@ -38,6 +42,14 @@ export const MainNavigator: FC<MainNavigatorProps> = observer(({ menu }) => {

menu ||= topNavBarMenu(i18n);

// 检查是否是 Open Library 路径
const isOpenLibraryPath = pathname.startsWith('/open-library');

// 如果是 Open Library 路径,不渲染主站导航栏
if (isOpenLibraryPath) {
return null;
}

return (
<Navbar bg="dark" variant="dark" fixed="top" expand="lg">
<Container>
Expand Down
103 changes: 103 additions & 0 deletions components/open-library/BookCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Link from 'next/link';
import React, { useContext } from 'react';
import { Button, Card, Image } from 'react-bootstrap';

import { I18nContext } from '../../models/Translation';

export interface Book {
id: number;
title: string;
author: string;
cover?: string;
status?: 'available' | 'borrowed';
category?: string;
description?: string;
}
Comment on lines +7 to +15
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

后端数据结构放在 /models 目录


interface BookCardProps {
book: Book;
showStatus?: boolean;
variant?: 'featured' | 'catalog';
}

const BookCard: React.FC<BookCardProps> = ({
book,
showStatus = false,
variant = 'featured',
}) => {
const cardClass =
variant === 'featured'
? 'h-100 shadow-sm border-0'
: 'h-100 shadow border-0';

const { t } = useContext(I18nContext);

return (
<Card className={cardClass}>
Comment on lines +17 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interface BookCardProps {
book: Book;
showStatus?: boolean;
variant?: 'featured' | 'catalog';
}
const BookCard: React.FC<BookCardProps> = ({
book,
showStatus = false,
variant = 'featured',
}) => {
const cardClass =
variant === 'featured'
? 'h-100 shadow-sm border-0'
: 'h-100 shadow border-0';
const { t } = useContext(I18nContext);
return (
<Card className={cardClass}>
import { FC } from 'react';
import { CardProps } from 'react-bootstrap';
export interface BookCardProps extends CardProps {
book: Book;
showStatus?: boolean;
variant?: 'featured' | 'catalog';
}
export const BookCard: FC<BookCardProps> = ({
className = '',
book,
showStatus = false,
variant = 'featured',
...cardProps
}) => {
const isFeatured = variant === 'featured';
const { t } = useContext(I18nContext);
return (
<Card className={`border-0 shadow${isFeatured ? '-sm': ''} ${className}`} {...cardProps}>

h-100 是与父组件之间的关系,应由组件的外部使用者决定,从 className 传入。

<div className="text-center p-3 position-relative">
{showStatus && (
<div className="position-absolute top-0 end-0 m-2">
<span
className={`badge ${
book.status === 'available'
? 'bg-success'
: 'bg-warning text-dark'
}`}
>
{book.status === 'available' ? '可借阅' : '已借出'}
</span>
Comment on lines +40 to +48
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</div>
)}
<div
className="d-flex align-items-center justify-content-center overflow-hidden rounded bg-light"
style={{ height: '180px' }}
>
<Image
src={book.cover || '/images/placeholder-book.svg'}
alt={`${book.title} 封面`}
className="img-fluid"
style={{
maxHeight: '160px',
maxWidth: '120px',
objectFit: 'contain',
}}
/>
</div>
Comment on lines +51 to +65
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</div>
<Card.Body className="text-center d-flex flex-column p-3">
<Card.Title
className="fw-bold h6 mb-2 text-truncate"
title={book.title}
>
{book.title}
</Card.Title>
<Card.Text
className="text-muted small mb-2 text-truncate"
title={book.author}
>
{book.author}
</Card.Text>
{book.category && (
<Card.Text className="text-muted small mb-3">
<i className="bi bi-tag me-1" />
{book.category}
</Card.Text>
)}
<div className="mt-auto">
<Link href={`/open-library/book/${book.id}`} passHref legacyBehavior>
<Button
variant="outline-success"
size="sm"
as="a"
className="rounded-pill px-3 fw-medium"
>
{t('view_details')}
</Button>
</Link>
</div>
</Card.Body>
</Card>
);
};

export default BookCard;
70 changes: 70 additions & 0 deletions components/open-library/FeaturedBooks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import Link from 'next/link';
import React from 'react';
import { Button, Col, Row } from 'react-bootstrap';

import BookCard, { Book } from './BookCard';
import { ContentContainer } from './Layout';

interface FeaturedBooksProps {
books: Book[];
title?: string;
subtitle?: string;
showViewAll?: boolean;
viewAllLink?: string;
viewAllText?: string;
}

const FeaturedBooks: React.FC<FeaturedBooksProps> = ({
books,
title = '精选图书',
subtitle = '社区成员推荐的优质读物,涵盖技术、设计、创业等多个领域',
showViewAll = true,
viewAllLink = '/open-library/books',
viewAllText = '查看全部图书',
}) => (
<section className="py-5 bg-light">
<ContentContainer>
<div className="text-center mb-5">
<h2 className="display-5 fw-bold text-dark mb-3 position-relative">
{title}
<div className="position-absolute start-50 translate-middle-x mt-2">
<div
className="bg-success rounded-pill"
style={{ width: '60px', height: '3px' }}
/>
</div>
</h2>
<p className="lead text-muted mx-auto" style={{ maxWidth: '600px' }}>
{subtitle}
</p>
</div>
Comment on lines +27 to +40
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div className="text-center mb-5">
<h2 className="display-5 fw-bold text-dark mb-3 position-relative">
{title}
<div className="position-absolute start-50 translate-middle-x mt-2">
<div
className="bg-success rounded-pill"
style={{ width: '60px', height: '3px' }}
/>
</div>
</h2>
<p className="lead text-muted mx-auto" style={{ maxWidth: '600px' }}>
{subtitle}
</p>
</div>
<hgroup className="text-center mb-5">
<h2 className="display-5 fw-bold text-dark mb-3 position-relative">
{title}
<div className="position-absolute start-50 translate-middle-x mt-2">
<div
className="bg-success rounded-pill"
style={{ width: '60px', height: '3px' }}
/>
</div>
</h2>
<p className="lead text-muted mx-auto" style={{ maxWidth: '37.5rem' }}>
{subtitle}
</p>
</hgroup>


<Row xs={1} sm={2} lg={3} xl={4} className="g-4 justify-content-center">
{books.slice(0, 8).map(book => (
<Col key={book.id}>
<BookCard book={book} variant="featured" />
</Col>
))}
</Row>

{showViewAll && (
<div className="text-center mt-5">
<Link href={viewAllLink} passHref legacyBehavior>
<Button
variant="outline-success"
size="lg"
as="a"
className="rounded-pill px-4 fw-semibold"
>
<i className="bi bi-collection me-2" />
{viewAllText}
<i className="bi bi-arrow-right ms-2" />
Comment on lines +59 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

</Button>
</Link>
</div>
)}
</ContentContainer>
</section>
);

export default FeaturedBooks;
116 changes: 116 additions & 0 deletions components/open-library/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import React, { PropsWithChildren, useContext } from 'react';
import { Col, Nav, Row } from 'react-bootstrap';

import { I18nContext } from '../../models/Translation';

// 使用 Bootstrap 工具类替换内联样式的 ContentContainer
const ContentContainer: React.FC<PropsWithChildren> = ({ children }) => (
<div className="container-xl px-3">{children}</div>
);
Comment on lines +6 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

结构这么简单的组件没必要,直接在具体页面用 <Container /> 再传参即可:https://react-bootstrap.github.io/docs/layout/grid/#container


const FooterComponent = () => {
// Use client-side rendering for the copyright text to avoid hydration issues
const [isMounted, setIsMounted] = React.useState(false);

const { t } = useContext(I18nContext);

React.useEffect(() => {
setIsMounted(true);
}, []);

return (
<footer className="bg-dark text-light py-4">
<ContentContainer>
<Row>
<Col md={4} className="mb-3 mb-md-0">
<h5 className="fw-bold mb-3">{t('open_library')}</h5>
<p className="text-light opacity-75 lh-base">
{t('footer_description')}
</p>
<div className="mt-3">
<a
href="#github"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-github me-1" />
GitHub
</a>
<a
href="#twitter"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-twitter me-1" />
Twitter
</a>
<a
href="#feishu"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-chat-dots me-1" />
Feishu
</a>
Comment on lines +31 to +51
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<a
href="#github"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-github me-1" />
GitHub
</a>
<a
href="#twitter"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-twitter me-1" />
Twitter
</a>
<a
href="#feishu"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-chat-dots me-1" />
Feishu
</a>
<a
href="https://applink.feishu.cn/client/chat/chatter/add_by_link?link_token=09blde41-7347-4f6b-9436-fa5efb7f4c6f"
className="text-light text-decoration-none me-3 hover-opacity"
>
<i className="bi bi-chat-dots me-1" />
Feishu
</a>

没有具体联系方式的就先不写。

</div>
</Col>
<Col md={3} className="mb-3 mb-md-0">
<h5 className="fw-bold mb-3">{t('quick_links_footer')}</h5>
<Nav className="flex-column">
<Nav.Link
href="/open-library/books"
className="text-light px-0 py-1 text-decoration-none"
>
<i className="bi bi-book me-2" />
{t('catalog_footer')}
</Nav.Link>
<Nav.Link
href="/open-library/how-to-borrow"
className="text-light px-0 py-1 text-decoration-none"
>
<i className="bi bi-info-circle me-2" />
{t('how_to_borrow')}
</Nav.Link>
</Nav>
</Col>
<Col md={5}>
<h5 className="fw-bold mb-3">{t('contact')}</h5>
<div className="text-light opacity-75">
<p className="mb-2">
<i className="bi bi-geo-alt me-2" />
freeCodeCamp Chengdu Community
</p>
<p className="mb-2">
<i className="bi bi-pin-map me-2" />
Chengdu, Sichuan, China
</p>
<p className="mb-2">
<i className="bi bi-envelope me-2" />
Email: [email protected]
</p>
<p className="mb-0">
<i className="bi bi-wechat me-2" />
WeChat: FCCChengdu
</p>
</div>
Comment on lines +75 to +92
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<div className="text-light opacity-75">
<p className="mb-2">
<i className="bi bi-geo-alt me-2" />
freeCodeCamp Chengdu Community
</p>
<p className="mb-2">
<i className="bi bi-pin-map me-2" />
Chengdu, Sichuan, China
</p>
<p className="mb-2">
<i className="bi bi-envelope me-2" />
Email: contact@openlibrary.org
</p>
<p className="mb-0">
<i className="bi bi-wechat me-2" />
WeChat: FCCChengdu
</p>
</div>
<ul className="list-unstyled d-flex flex-column gap-2 text-light opacity-75">
<li>
<Icon name="geo-alt" className="me-2" />
freeCodeCamp Chengdu Community
</li>
<li>
<i className="bi bi-pin-map me-2" />
Chengdu, Sichuan, China
</li>
<li>
<i className="bi bi-envelope me-2" />
Email: <a href="mailto:[email protected]">[email protected]</a>
</li>
<li>
<i className="bi bi-wechat me-2" />
<a target="_blank" href="https://open.weixin.qq.com/qr/code?username=gh_b8b06d05cfa6">
WeChat: freeCodeCode 成都社区
</a>
</li>
</ul>

前两行的地址翻译一下

</Col>
</Row>

<hr className="mt-4 mb-3 border-secondary opacity-25" />

<div className="text-center text-light opacity-75 py-2">
{isMounted ? (
<>
&copy; {new Date().getFullYear()} {t('open_library')}.{' '}
{t('all_rights_reserved')}
</>
) : (
<>
&copy; {new Date().getFullYear()} Open Library. All rights
reserved.
</>
)}
</div>
</ContentContainer>
</footer>
);
};

export default FooterComponent;
Loading
Loading