Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
80 changes: 80 additions & 0 deletions components/IssueCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { FC, useContext } from 'react';
import { Badge, Card } from 'react-bootstrap';

import type { Issue } from '../models/Base';
import { I18nContext } from '../models/Translation';
import styles from '../styles/Weekly.module.less';

interface IssueCardProps {
issue: Issue;
}

export const IssueCard: FC<IssueCardProps> = ({ issue }) => {
const { t } = useContext(I18nContext);

return (
<Card className={`h-100 shadow-sm ${styles.issueCard}`}>
<Card.Body className="d-flex flex-column">
<div className="d-flex justify-content-between align-items-start mb-3">
<Badge bg={issue.state === 'open' ? 'success' : 'secondary'}>
{issue.state === 'open' ? t('open') : t('closed')}
</Badge>
<small className="text-muted">#{issue.number}</small>
</div>

<Card.Title className="h5">
<a
href={`/weekly/${issue.number}`}
className={`text-decoration-none text-dark ${styles.issueTitle}`}
>
{issue.title}
</a>
</Card.Title>

{issue.body && (
<Card.Text className="text-muted mb-3 flex-grow-1">
{issue.body.slice(0, 150)}
{issue.body.length > 150 && '...'}
</Card.Text>
)}

<div className="mt-auto">
{issue.labels && issue.labels.length > 0 && (
<div className="mb-2">
{issue.labels.slice(0, 3).map((label, index) => (
<Badge
key={index}
bg="light"
text="dark"
className={`me-1 ${styles.labelBadge}`}
>
{typeof label === 'string' ? label : label.name}
</Badge>
))}
{issue.labels.length > 3 && (
<Badge bg="light" text="dark" className="small">
+{issue.labels.length - 3}
</Badge>
)}
</div>
)}

<div
className={`d-flex justify-content-between align-items-center text-small text-muted ${styles.authorInfo}`}
>
<span>
{issue.user && (
<>
{t('weekly_author')}: {issue.user.login}
</>
)}
</span>
<time dateTime={issue.created_at}>
{new Date(issue.created_at).toLocaleDateString('zh-CN')}
</time>
</div>
</div>
</Card.Body>
</Card>
);
};
2 changes: 2 additions & 0 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const MainNavigator: FC = observer(() => {

<Nav.Link href="/activity">{t('activity')}</Nav.Link>

<Nav.Link href="/weekly">{t('weekly')}</Nav.Link>

<Nav.Link href="/community">{t('community')}</Nav.Link>

<Nav.Link href="/article/Wiki/_posts/Profile/about">{t('about')}</Nav.Link>
Expand Down
5 changes: 3 additions & 2 deletions models/Base.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'core-js/full/array/from-async';

import { HTTPClient } from 'koajax';
import { githubClient, RepositoryModel } from 'mobx-github';
import { githubClient, RepositoryModel, Issue as GitHubIssue } from 'mobx-github';
import { TableCellAttachment, TableCellMedia, TableCellValue } from 'mobx-lark';
import { DataObject } from 'mobx-restful';
import { isEmpty } from 'web-utility';
Expand Down Expand Up @@ -30,7 +30,8 @@ githubClient.use(({ request }, next) => {
return next();
});

export { githubClient };
export { githubClient, RepositoryModel };
export type { GitHubIssue as Issue };

export const githubRawClient = new HTTPClient({
baseURI: `${ProxyBaseURL}/raw.githubusercontent.com/`,
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.44.0",
"file-type": "^21.0.0",
"github-markdown-css": "^5.8.1",
"idea-react": "^2.0.0-rc.13",
"koa": "^2.16.1",
"koajax": "^3.1.2",
Expand Down
179 changes: 179 additions & 0 deletions pages/weekly/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import 'github-markdown-css/github-markdown-light.css';

import { marked } from 'marked';
import { observer } from 'mobx-react';
import { GetStaticPaths, GetStaticProps } from 'next';
import { ParsedUrlQuery } from 'querystring';
import { FC, useContext } from 'react';
import { Badge, Breadcrumb, Button, Card, Container } from 'react-bootstrap';

import { PageHead } from '../../components/Layout/PageHead';
import type { Issue } from '../../models/Base';
import { RepositoryModel } from '../../models/Base';
import { I18nContext } from '../../models/Translation';
import styles from '../../styles/Weekly.module.less';

interface WeeklyDetailParams extends ParsedUrlQuery {
id: string;
}

interface WeeklyDetailProps {
issue: Issue;
}

export const getStaticPaths: GetStaticPaths<WeeklyDetailParams> = async () => {
const repository = new RepositoryModel('FreeCodeCamp-Chengdu');
const repo = await repository.getOne('IT-Technology-weekly', ['issues']);

const paths = (repo.issues || []).map(issue => ({
params: { id: issue.number.toString() },
}));

return { paths, fallback: 'blocking' };
};

export const getStaticProps: GetStaticProps<WeeklyDetailProps, WeeklyDetailParams> = async ({
params,
}) => {
const { id } = params!;
const repository = new RepositoryModel('FreeCodeCamp-Chengdu');
const repo = await repository.getOne('IT-Technology-weekly', ['issues']);

const issue = repo.issues?.find(issue => issue.number.toString() === id);

if (!issue) {
return {
notFound: true,
};
}

return {
props: {
issue: JSON.parse(JSON.stringify(issue)),
},
revalidate: 3600, // Revalidate every hour
};
};

const WeeklyDetailPage: FC<WeeklyDetailProps> = observer(({ issue }) => {
const { t } = useContext(I18nContext);
const htmlContent = issue.body ? (marked(issue.body) as string) : '';

return (
<Container className={`py-4 ${styles.weeklyContainer}`}>
<PageHead
title={`${issue.title} - ${t('weekly')}`}
description={issue.body ? issue.body.substring(0, 160) + '...' : issue.title}
/>

<Breadcrumb className="mb-4">
<Breadcrumb.Item href="/">{t('home_page')}</Breadcrumb.Item>
<Breadcrumb.Item href="/weekly">{t('weekly')}</Breadcrumb.Item>
<Breadcrumb.Item active>#{issue.number}</Breadcrumb.Item>
</Breadcrumb>

<article>
<header className="mb-4">
<div className="d-flex justify-content-between align-items-start mb-3">
<Badge bg={issue.state === 'open' ? 'success' : 'secondary'} className="fs-6">
{issue.state === 'open' ? t('open') : t('closed')}
</Badge>
<span className="text-muted">#{issue.number}</span>
</div>

<h1 className="display-5 mb-3">{issue.title}</h1>

{issue.labels?.[0] && (
<ul className="list-unstyled mb-3">
{issue.labels.map((label, index) => (
<li key={index} className="d-inline-block me-2 mb-2">
<Badge
bg="light"
text="dark"
className={styles.labelBadge}
>
{typeof label === 'string' ? label : label.name}
</Badge>
</li>
))}
</ul>
)}

<dl className="row mb-4 pb-3 border-bottom">
{issue.user && (
<>
<dt className="col-sm-3 text-muted">{t('weekly_author')}:</dt>
<dd className="col-sm-9">
<strong>{issue.user.login}</strong>
</dd>
</>
)}
{issue.created_at && (
<>
<dt className="col-sm-3 text-muted">{t('weekly_published')}:</dt>
<dd className="col-sm-9">
<time dateTime={issue.created_at}>
{new Date(issue.created_at).toLocaleString('zh-CN')}
</time>
</dd>
</>
)}
{issue.updated_at && issue.updated_at !== issue.created_at && (
<>
<dt className="col-sm-3 text-muted">{t('weekly_updated')}:</dt>
<dd className="col-sm-9">
<time dateTime={issue.updated_at}>
{new Date(issue.updated_at).toLocaleString('zh-CN')}
</time>
</dd>
</>
)}
</dl>
<div className="d-flex justify-content-end mb-4">
<Button
variant="outline-primary"
size="sm"
href={issue.html_url}
target="_blank"
rel="noopener noreferrer"
>
{t('view_on_github')}
</Button>
</div>
</header>

{htmlContent ? (
<div
dangerouslySetInnerHTML={{ __html: htmlContent }}
className="markdown-body"
/>
) : (
<Card>
<Card.Body className="text-center text-muted">
<p>{t('weekly_issue_no_content')}</p>
</Card.Body>
</Card>
)}
</article>

<footer className="mt-5 pt-4 border-top">
<div className="d-flex justify-content-between align-items-center">
<Button variant="outline-secondary" href="/weekly">
← {t('back_to_weekly_list')}
</Button>

<div className="text-muted small">
<p>
{t('github_document_description')}
<a href={issue.html_url} target="_blank" rel="noopener noreferrer" className="ms-1">
{t('view_original_on_github')}
</a>
</p>
</div>
</div>
</footer>
</Container>
);
});

export default WeeklyDetailPage;
82 changes: 82 additions & 0 deletions pages/weekly/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { observer } from 'mobx-react';
import { GetStaticProps, InferGetStaticPropsType } from 'next';
import { FC, useContext } from 'react';
import { Button, Card, Col, Container, Row } from 'react-bootstrap';

import { IssueCard } from '../../components/IssueCard';
import { PageHead } from '../../components/Layout/PageHead';
import type { Issue } from '../../models/Base';
import { RepositoryModel } from '../../models/Base';
import { I18nContext } from '../../models/Translation';
import styles from '../../styles/Weekly.module.less';

interface WeeklyPageProps {
issues: Issue[];
}

export const getStaticProps: GetStaticProps<WeeklyPageProps> = async () => {
const repository = new RepositoryModel('FreeCodeCamp-Chengdu');
const repo = await repository.getOne('IT-Technology-weekly', ['issues']);

return {
props: {
issues: JSON.parse(JSON.stringify(repo.issues || [])),
},
revalidate: 3600, // Revalidate every hour
};
};

const WeeklyIndexPage: FC<InferGetStaticPropsType<typeof getStaticProps>> = observer(
({ issues }) => {
const { t } = useContext(I18nContext);

return (
<Container className="py-5">
<PageHead title={t('weekly')} description={t('weekly_description')} />

<div className={styles.weeklyHeader}>
<h1>{t('weekly')}</h1>
<p className="lead">{t('weekly_description')}</p>
<Button
variant="outline-light"
size="lg"
href="https://github.com/FreeCodeCamp-Chengdu/IT-Technology-weekly"
target="_blank"
rel="noopener noreferrer"
>
{t('view_on_github')}
</Button>
</div>

{issues.length > 0 ? (
<Row xs={1} md={2} lg={3} className="g-4">
{issues.map((issue) => (
<Col key={issue.id}>
<IssueCard
issue={issue}
/>
</Col>
))}
</Row>
) : (
<Card className="text-center">
<Card.Body>
<h5>{t('no_weekly_content')}</h5>
<p className="text-muted">{t('weekly_content_from_github')}</p>
<Button
variant="primary"
href="https://github.com/FreeCodeCamp-Chengdu/IT-Technology-weekly/issues"
target="_blank"
rel="noopener noreferrer"
>
{t('view_all_issues')}
</Button>
</Card.Body>
</Card>
)}
</Container>
);
},
);

export default WeeklyIndexPage;
Loading