Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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: 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
200 changes: 200 additions & 0 deletions pages/weekly/[id].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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 { githubClient } from '../../models/Base';
import { I18nContext } from '../../models/Translation';
import styles from '../../styles/Weekly.module.less';

// GitHub Issue type definition
interface GitHubIssue {
id: number;
number: number;
title: string;
body: string | null;
state: 'open' | 'closed';
labels: Array<{ name: string; color: string } | string>;
user: {
login: string;
avatar_url: string;
} | null;
created_at: string;
updated_at: string;
html_url: string;
}

interface WeeklyDetailParams extends ParsedUrlQuery {
id: string;
}

interface WeeklyDetailProps {
issue: GitHubIssue;
}

export const getStaticPaths: GetStaticPaths<WeeklyDetailParams> = async () => {
try {
const { body: issues } = await githubClient.get<GitHubIssue[]>(
'repos/FreeCodeCamp-Chengdu/IT-Technology-weekly/issues?state=all&sort=created&direction=desc',
);
Comment thread
TechQuery marked this conversation as resolved.
Outdated

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

return { paths, fallback: 'blocking' };
} catch (error) {
console.error('Failed to generate static paths:', error);

return { paths: [], fallback: 'blocking' };
Comment thread
TechQuery marked this conversation as resolved.
Outdated
}
};

export const getStaticProps: GetStaticProps<WeeklyDetailProps, WeeklyDetailParams> = async ({
params,
}) => {
const { id } = params!;

try {
const { body: issue } = await githubClient.get<GitHubIssue>(
`repos/FreeCodeCamp-Chengdu/IT-Technology-weekly/issues/${id}`,
);

if (!issue) {
return {
notFound: true,
};
}
Comment thread
TechQuery marked this conversation as resolved.
Outdated

return {
props: {
issue: JSON.parse(JSON.stringify(issue)),
},
revalidate: 3600, // Revalidate every hour
};
} catch (error) {
console.error('Failed to fetch issue:', error);

return {
notFound: true,
};
}
Comment thread
TechQuery marked this conversation as resolved.
Outdated
};

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}
Comment thread
TechQuery marked this conversation as resolved.
Outdated
/>

<Breadcrumb className="mb-4">
<Breadcrumb.Item href="/">首页</Breadcrumb.Item>
Comment thread
TechQuery marked this conversation as resolved.
Outdated
<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 && issue.labels.length > 0 && (
Comment thread
TechQuery marked this conversation as resolved.
Outdated
<div className="mb-3">
{issue.labels.map((label, index) => (
<Badge
key={index}
bg="light"
text="dark"
className={`me-2 mb-2 ${styles.labelBadge}`}
>
{typeof label === 'string' ? label : label.name}
</Badge>
))}
</div>
Comment thread
TechQuery marked this conversation as resolved.
Outdated
)}

<div className="d-flex justify-content-between align-items-center mb-4 pb-3 border-bottom">
<div className="text-muted">
{issue.user && (
<span>
{t('weekly_author')}: <strong>{issue.user.login}</strong>
</span>
)}
{issue.created_at && (
<span className="ms-3">
{t('weekly_published')}:{' '}
<time dateTime={issue.created_at}>
{new Date(issue.created_at).toLocaleString('zh-CN')}
</time>
</span>
)}
{issue.updated_at && issue.updated_at !== issue.created_at && (
<span className="ms-3">
{t('weekly_updated')}:{' '}
<time dateTime={issue.updated_at}>
{new Date(issue.updated_at).toLocaleString('zh-CN')}
</time>
</span>
)}
</div>
Comment thread
TechQuery marked this conversation as resolved.
Outdated

<div className="d-flex gap-2">
<Button
variant="outline-primary"
size="sm"
href={issue.html_url}
target="_blank"
rel="noopener noreferrer"
>
{t('view_on_github')}
</Button>
</div>
</div>
</header>

{htmlContent ? (
<div dangerouslySetInnerHTML={{ __html: htmlContent }} className={styles.markdownBody} />
) : (
<Card>
<Card.Body className="text-center text-muted">
<p>{t('weekly_issue_no_content')}</p>
</Card.Body>
</Card>
Comment thread
TechQuery marked this conversation as resolved.
Outdated
)}
</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 className="mb-0">
{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>
Comment thread
TechQuery marked this conversation as resolved.
Outdated
</div>
</footer>
</Container>
);
});

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

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

// GitHub Issue type definition
interface GitHubIssue {
id: number;
number: number;
title: string;
body: string | null;
state: 'open' | 'closed';
labels: Array<{ name: string; color: string } | string>;
user: {
login: string;
avatar_url: string;
} | null;
created_at: string;
updated_at: string;
html_url: string;
}

interface WeeklyPageProps {
issues: GitHubIssue[];
}

export const getStaticProps: GetStaticProps<WeeklyPageProps> = async () => {
try {
const { body: issues } = await githubClient.get<GitHubIssue[]>(
'repos/FreeCodeCamp-Chengdu/IT-Technology-weekly/issues?state=all&sort=created&direction=desc',
);

return {
props: {
issues: JSON.parse(JSON.stringify(issues || [])),
},
revalidate: 3600, // Revalidate every hour
};
} catch (error) {
console.error('Failed to fetch issues:', error);

return {
props: {
issues: [],
},
revalidate: 300, // Retry more frequently if there's an error
};
}
};

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 => (
Comment thread
TechQuery marked this conversation as resolved.
Outdated
<Col key={issue.id}>
<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.substring(0, 150)}
Comment thread
TechQuery marked this conversation as resolved.
Outdated
{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>
</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