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
3 changes: 2 additions & 1 deletion components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
},
],
},
{ href: '/bounty', title: t('bounty') },
{ href: '/bounty', title: t('bounty') }
{ href: '/library', title: t('open_library') },
{
title: t('NGO'),
subs: [
Expand Down
112 changes: 112 additions & 0 deletions pages/library/[id].tsx
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 } };
});

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">
&larr; {t("back_to_library")}
</a>

<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">&larr; {t("back_to_library")}</a>
</Col>
</Row>
</Container>
);
});

export default BookDetailPage;
147 changes: 147 additions & 0 deletions pages/library/books.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type { Book } from '../../types/library';

export const libraryBooks: Book[] = [
{
id: 'the-cathedral-and-the-bazaar',
title: 'The Cathedral and the Bazaar',
author: 'Eric S. Raymond',
cover: 'https://images-na.ssl-images-amazon.com/images/I/51G9NqRf1-L._SX331_BO1,204,203,200_.jpg',
description: 'A classic essay on open source development methodology.',
category: 'open-source',
status: 'available',
publisher: "O'Reilly Media",
year: 1999,
tags: ['open source', 'classic', 'methodology'],
},
{
id: 'producing-open-source-software',
title: 'Producing Open Source Software',
author: 'Karl Fogel',
cover: 'https://producingoss.com/images/producingoss-cover-small.gif',
description: 'How to Run a Successful Free Software Project.',
category: 'open-source',
status: 'available',
publisher: "O'Reilly Media",
year: 2005,
tags: ['community', 'project management'],
},
{
id: 'the-open-source-way',
title: 'The Open Source Way',
author: 'Red Hat Community',
cover: 'https://www.theopensourceway.org/images/cover.png',
description: 'Guide to managing and participating in open source communities.',
category: 'open-source',
status: 'borrowed',
borrower: 'Zhang San',
borrowDate: '2026-06-15',
returnDate: '2026-07-15',
tags: ['community guide', 'Red Hat'],
},
{
id: 'clean-code',
title: 'Clean Code',
author: 'Robert C. Martin',
cover: 'https://images-na.ssl-images-amazon.com/images/I/41xShlnTZTL._SX376_BO1,204,203,200_.jpg',
description: 'A Handbook of Agile Software Craftsmanship.',
category: 'programming',
status: 'available',
publisher: 'Pearson',
year: 2008,
tags: ['code quality', 'refactoring', 'agile'],
},
{
id: 'design-patterns',
title: 'Design Patterns',
author: 'Gamma, Helm, Johnson, Vlissides',
cover: 'https://images-na.ssl-images-amazon.com/images/I/51kuc0iWoEL._SX379_BO1,204,203,200_.jpg',
description: 'Elements of Reusable Object-Oriented Software.',
category: 'programming',
status: 'borrowed',
borrower: 'Li Si',
borrowDate: '2026-06-20',
returnDate: '2026-08-20',
tags: ['OOP', 'architecture', 'classic'],
},
{
id: 'art-of-community',
title: 'The Art of Community',
author: 'Jono Bacon',
cover: 'https://www.artofcommunityonline.org/images/book-cover.jpg',
description: 'Building the New Age of Participation.',
category: 'community',
status: 'available',
publisher: "O'Reilly Media",
year: 2009,
tags: ['community building', 'leadership'],
},
{
id: 'working-in-public',
title: 'Working in Public',
author: 'Nadia Eghbal',
cover: 'https://images-na.ssl-images-amazon.com/images/I/41PFTZYKCAL._SX329_BO1,204,203,200_.jpg',
description: 'The Making and Maintenance of Open Source Software.',
category: 'open-source',
status: 'available',
publisher: 'Stripe Press',
year: 2020,
tags: ['open source ecosystem', 'maintainers'],
},
{
id: 'refactoring',
title: 'Refactoring',
author: 'Martin Fowler',
cover: 'https://images-na.ssl-images-amazon.com/images/I/41odjOW0AVL._SX379_BO1,204,203,200_.jpg',
description: 'Improving the Design of Existing Code.',
category: 'programming',
status: 'available',
publisher: 'Addison-Wesley',
year: 2018,
tags: ['code quality', 'design'],
},
{
id: 'open-source-licenses',
title: 'Understanding Open Source Licensing',
author: 'Andrew M. St. Laurent',
cover: 'https://images-na.ssl-images-amazon.com/images/I/51vDk3qNWQL._SX379_BO1,204,203,200_.jpg',
description: 'Guide to legal and practical aspects of open source licensing.',
category: 'open-source',
status: 'borrowed',
borrower: 'Wang Wu',
borrowDate: '2026-06-01',
returnDate: '2026-07-01',
tags: ['licenses', 'legal', 'compliance'],
},
{
id: 'think-like-a-commoner',
title: 'Think Like a Commoner',
author: 'David Bollier',
cover: 'https://images-na.ssl-images-amazon.com/images/I/41TjkXY6nkL._SX331_BO1,204,203,200_.jpg',
description: 'A Short Introduction to the Commons.',
category: 'community',
status: 'available',
tags: ['commons', 'sharing economy'],
},
{
id: 'dont-make-me-think',
title: "Don't Make Me Think",
author: 'Steve Krug',
cover: 'https://images-na.ssl-images-amazon.com/images/I/51WS36aVMbL._SX387_BO1,204,203,200_.jpg',
description: 'A Common Sense Approach to Web Usability.',
category: 'design',
status: 'available',
publisher: 'New Riders',
year: 2014,
tags: ['UX', 'usability', 'web design'],
},
{
id: 'lean-startup',
title: 'The Lean Startup',
author: 'Eric Ries',
cover: 'https://images-na.ssl-images-amazon.com/images/I/41GQjLsSN5L._SX329_BO1,204,203,200_.jpg',
description: 'How Today Entrepreneurs Use Continuous Innovation.',
category: 'business',
status: 'available',
tags: ['startup', 'innovation', 'methodology'],
},
];
120 changes: 120 additions & 0 deletions pages/library/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { observer } from 'mobx-react';
import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { FC, useContext, useMemo, useState } from 'react';
import { Badge, Button, Card, Col, Container, Form, Row } from 'react-bootstrap';
import Link from 'next/link';

import { PageHead } from '../../components/Layout/PageHead';
import { SectionTitle } from '../../components/Layout/SectionTitle';
import { I18nContext } from '../../models/Translation';
import type { Book, BookCategory, BookStatus } from '../../types/library';
import { libraryBooks } from './books';

export const getServerSideProps = compose(cache(), errorLogger, async () => ({
props: {} as Record<string, never>,
}));

const CATEGORY_OPTIONS: { value: BookCategory | 'all'; labelKey: string }[] = [
{ value: 'all', labelKey: 'library_category_all' },
{ value: 'open-source', labelKey: 'library_category_open_source' },
{ value: 'programming', labelKey: 'library_category_programming' },
{ value: 'community', labelKey: 'library_category_community' },
{ value: 'design', labelKey: 'library_category_design' },
{ value: 'business', labelKey: 'library_category_business' },
];

const LibraryPage: FC = observer(() => {
const { t } = useContext(I18nContext);
const [category, setCategory] = useState<BookCategory | 'all'>('all');
const [search, setSearch] = useState('');

const filteredBooks = useMemo(() => {
let books: Book[] = libraryBooks;
if (category !== 'all') {
books = books.filter((b) => b.category === category);
}
if (search.trim()) {
const q = search.toLowerCase();
books = books.filter(
(b) =>
b.title.toLowerCase().includes(q) ||
b.author.toLowerCase().includes(q) ||
b.tags?.some((tag) => tag.toLowerCase().includes(q)),
);
}
return books;
}, [category, search]);

return (
<Container>
<PageHead title={t('open_library')} description={t('open_library_description')} />
<h1 className="py-5 text-center text-md-start ps-md-4">{t('open_library')}</h1>

<section className="mb-4">
<p className="text-muted">{t('open_library_description')}</p>
<Row className="g-3 mb-4">
<Col xs={12} md={4}>
<Form.Control
type="search"
placeholder={t('search_books_placeholder')}
value={search}
onChange={(e) => setSearch((e.target as HTMLInputElement).value)}
/>
</Col>
<Col xs={6} md={3}>
<Form.Select
value={category}
onChange={(e) => setCategory((e.target as HTMLSelectElement).value as BookCategory | 'all')}
>
{CATEGORY_OPTIONS.map(({ value, labelKey }) => (
<option key={value} value={value}>{t(labelKey as any)}</option>
))}
</Form.Select>
</Col>
</Row>
</section>

<SectionTitle count={filteredBooks.length}>{t('book_catalog')}</SectionTitle>
<Row as="section" className="g-4 mb-5" xs={1} sm={2} lg={3} xl={4}>
{filteredBooks.map((book) => (
<Col key={book.id}>
<Card className="h-100 shadow-sm">
<div className="text-center pt-4 px-3 bg-light">
<Card.Img
variant="top"
src={book.cover}
alt={book.title}
style={{ width: 120, height: 160, objectFit: 'cover' }}
className="rounded"
/>
</div>
<Card.Body className="d-flex flex-column">
<Card.Title className="fs-6">{book.title}</Card.Title>
<Card.Text className="text-muted small mb-2">{book.author}</Card.Text>
<div className="d-flex gap-2 flex-wrap mb-2">
<Badge bg={book.status === 'available' ? 'success' : 'secondary'}>
{t(book.status === 'available' ? 'library_status_available' : 'library_status_borrowed' as any)}
</Badge>
{book.tags?.slice(0, 2).map((tag) => (
<Badge key={tag} bg="info" className="bg-opacity-50 text-dark">{tag}</Badge>
))}
</div>
<div className="mt-auto">
<Link href={`/library/${book.id}`} className="btn btn-outline-primary btn-sm w-100 stretched-link" passHref legacyBehavior><Button variant="outline-primary" size="sm" className="w-100">
{t('view_details')}
</Button></Link>
</div>
</Card.Body>
</Card>
</Col>
))}
</Row>

{filteredBooks.length === 0 && (
<div className="text-center py-5 text-muted">{t('no_books_found')}</div>
)}
</Container>
);
});

export default LibraryPage;
Loading