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;
}

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}>
<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>
</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>
</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>

<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" />
</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>
);

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>
</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: contact@openlibrary.org
</p>
<p className="mb-0">
<i className="bi bi-wechat me-2" />
WeChat: FCCChengdu
</p>
</div>
</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