Skip to content
Open
Show file tree
Hide file tree
Changes from 8 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 @@ -23,6 +23,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 @@ -36,6 +40,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