Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
25 changes: 25 additions & 0 deletions components/Activity/CommentBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Giscus from '@giscus/react';
import { observer } from 'mobx-react';
import { useContext } from 'react';

import { I18nContext } from '../../models/Translation';

export const CommentBox = observer(() => {
const { currentLanguage } = useContext(I18nContext);

return (
<Giscus
repo="Open-Source-Bazaar/Open-Source-Bazaar.github.io"
repoId="R_kgDOGzCrLg"
category="Comments"
categoryId="DIC_kwDOGzCrLs4C0g_6"
mapping="pathname"
strict="0"
reactionsEnabled="1"
emitMetadata="0"
inputPosition="bottom"
theme="preferred_color_scheme"
lang={currentLanguage.startsWith('zh-') ? currentLanguage : currentLanguage.split('-')[0]}
/>
);
});
44 changes: 44 additions & 0 deletions components/Activity/ProductCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { observer } from 'mobx-react';
import { FilePreview } from 'mobx-restful-table';
import { FC } from 'react';
import { CardProps, Card, Button } from 'react-bootstrap';
import { formatDate } from 'web-utility';

import { Product } from '../../models/Hackathon';

export type ProductCardProps = Product & Omit<CardProps, 'id' | 'title'>;

export const ProductCard: FC<ProductCardProps> = observer(
({ className = '', id, createdAt, name, sourceLink, link = sourceLink, summary, ...props }) => (
<Card className={`border-success ${className}`} {...props}>
<Card.Body className="d-flex flex-column">
<Card.Title
as="a"
className="text-primary"
title={name as string}
target="_blank"
href={link as string}
>
{(name || link) as string}
</Card.Title>
<p className="border-bottom p-2 text-muted text-truncate">{summary as string}</p>
<div className="border-bottom py-2 my-2 flex-fill">
<FilePreview className="w-100" path={link as string} />

{sourceLink && (
<Button variant="success" size="sm" href={sourceLink as string}>
🔗 Git
</Button>
)}
</div>
<time
className="d-block p-2 text-truncate"
dateTime={new Date(createdAt as number).toJSON()}
>
📅
{formatDate(createdAt as number)}
</time>
</Card.Body>
</Card>
),
);
18 changes: 16 additions & 2 deletions components/Navigator/MainNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { textJoin } from 'mobx-i18n';
import { observer } from 'mobx-react';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router';
Expand Down Expand Up @@ -39,11 +40,24 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [
subs: [
{ href: '/project', title: t('open_source_projects') },
{ href: '/issue', title: 'GitHub issues' },
{ href: '/license-filter', title: t('license_filter') },
],
},
{
title: t('hackathon'),
subs: [
{
href: 'https://github.com/Open-Source-Bazaar/Git-Hackathon-scaffold',
title: t('hackathon'),
title: textJoin('GitHub', t('hackathon')),
},
{
href: '/search/activity?keywords=Hackathon',
title: textJoin('Lark', t('hackathon')),
},
{
href: 'https://test.hackathon.fcc-cd.dev/open-source',
title: textJoin(t('hackathon'), t('open_source_projects')),
},
{ href: '/license-filter', title: t('license_filter') },
],
},
{
Expand Down
47 changes: 46 additions & 1 deletion models/Hackathon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
normalizeText,
TableCellRelation,
TableCellText,
TableCellUser,
TableCellValue,
TableRecord,
} from 'mobx-lark';
Expand All @@ -24,7 +25,7 @@ export class AgendaModel extends BiDataTable<Agenda>() {
return {
...meta,
...fields,
summary: normalizeText(summary as TableCellText),
summary: (summary as TableCellText[])!.map(normalizeText),
};
}
}
Expand Down Expand Up @@ -140,3 +141,47 @@ export class ProjectModel extends BiDataTable<Project>() {
};
}
}

export type Member = LarkBase &
Record<'summary' | 'person' | 'skills' | 'githubAccount' | 'project' | 'status', TableCellValue>;

export class MemberModel extends BiDataTable<Member>() {
client = larkClient;

queryOptions: BiDataQueryOptions = { text_field_as_array: false };

extractFields({
fields: { summary, person, skills, githubAccount, ...fields },
...meta
}: TableRecord<Member>) {
return {
...meta,
...fields,
person: (person as TableCellUser[])?.[0],
summary: (summary as TableCellText[])!.map(normalizeText),
skills: skills?.toString().split(/\s*,\s*/) || [],
githubAccount: normalizeText(githubAccount as TableCellText),
};
}
}

export type Product = LarkBase &
Record<
'name' | 'project' | 'template' | 'link' | 'sourceLink' | 'file' | 'summary',
TableCellValue
>;

export class ProductModel extends BiDataTable<Product>() {
client = larkClient;

queryOptions: BiDataQueryOptions = { text_field_as_array: false };

extractFields({ fields: { link, sourceLink, ...fields }, ...meta }: TableRecord<Product>) {
return {
...meta,
...fields,
link: normalizeText(link as TableCellText),
sourceLink: normalizeText(sourceLink as TableCellText),
};
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"test": "lint-staged && tsc --noEmit"
},
"dependencies": {
"@giscus/react": "^3.1.0",
"@koa/router": "^15.1.1",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
Expand Down
12 changes: 10 additions & 2 deletions pages/hackathon/[id].tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BiTableSchema, TableCellLocation, TableCellUser } from 'mobx-lark';
import { observer } from 'mobx-react';
import Link from 'next/link';
import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { FC, useContext } from 'react';
import { Badge, Card, Col, Container, Row } from 'react-bootstrap';
Expand Down Expand Up @@ -199,11 +200,18 @@ const HackathonDetail: FC<HackathonDetailProps> = observer(({ activity, hackatho
<h2 className={styles.sectionTitle}>💡 {t('projects')}</h2>

<Row as="ul" className="list-unstyled mt-4 g-3" md={2} lg={3} xl={4}>
{projects.map(({ name, score, summary, createdBy, members }) => (
{projects.map(({ id, name, score, summary, createdBy, members }) => (
<Col as="li" key={name as string}>
<Card className={styles.projectCard} body>
<div className="d-flex justify-content-between align-items-start mb-3">
<h6 className="text-white flex-grow-1">{name as string}</h6>
<h6 className="text-white flex-grow-1">
<Link
className="stretched-link"
href={`${ActivityModel.getLink(activity)}/team/${id}`}
>
{name as string}
</Link>
</h6>
<div className={styles.scoreCircle}>{score as number}</div>
</div>
<p className="text-white-50 small mb-3">{summary as string}</p>
Expand Down
188 changes: 188 additions & 0 deletions pages/hackathon/[id]/team/[tid].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { Avatar } from 'idea-react';
import { BiTableSchema, TableCellUser } from 'mobx-lark';
import { observer } from 'mobx-react';
import { cache, compose, errorLogger } from 'next-ssr-middleware';
import { FC, useContext, useState } from 'react';
import {
Breadcrumb,
Button,
Card,
Col,
Container,
Modal,
Ratio,
Row,
Tab,
Tabs,
} from 'react-bootstrap';

import { CommentBox } from '../../../../components/Activity/CommentBox';
import { ProductCard } from '../../../../components/Activity/ProductCard';
import { PageHead } from '../../../../components/Layout/PageHead';
import { Activity, ActivityModel } from '../../../../models/Activity';
import {
Member,
MemberModel,
Product,
ProductModel,
Project,
ProjectModel,
} from '../../../../models/Hackathon';
import { I18nContext } from '../../../../models/Translation';

export const getServerSideProps = compose<Record<'id' | 'tid', string>>(
cache(),
errorLogger,
async ({ params }) => {
const activity = await new ActivityModel().getOne(params!.id);

// @ts-expect-error Upstream compatibility
const { appId, tableIdMap } = activity.databaseSchema;

const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid);

// Get approved members for this project
const members = await new MemberModel(appId, tableIdMap.Member).getAll({
project: project.name as string,
status: 'approved',
});

// Get products for this project
const products = await new ProductModel(appId, tableIdMap.Product).getAll({
project: project.name as string,
});

return { props: { activity, project, members, products } };
},
);

interface ProjectPageProps {
activity: Activity;
project: Project;
members: Member[];
products: Product[];
}

const ProjectPage: FC<ProjectPageProps> = observer(({ activity, project, members, products }) => {
const { t } = useContext(I18nContext);
const [showScoreModal, setShowScoreModal] = useState(false);

const { name: activityName, databaseSchema } = activity;
const { formLinkMap } = databaseSchema as unknown as BiTableSchema;
const { name: displayName, summary: description, createdBy, score } = project;

const currentRoute = [
{ title: activityName as string, href: ActivityModel.getLink(activity) },
{ title: displayName as string },
];

return (
<Container as="main" className="mt-4">
<PageHead title={`${displayName} - ${activityName}`} />

<Breadcrumb aria-label="breadcrumb">
{currentRoute.map(({ title, href }, index, { length }) => {
const isActive = index === length - 1;

return (
<Breadcrumb.Item key={title} href={isActive ? undefined : href} active={isActive}>
{title}
</Breadcrumb.Item>
);
})}
</Breadcrumb>

<Row className="mt-4 g-3">
<Col xs={12} sm={4}>
<Card>
<Card.Header className="bg-white">
<h1 className="h3 my-2">{displayName as string}</h1>
<p className="text-muted">{description as string}</p>
{score != null && (
<div className="text-center mt-3">
<Button variant="danger" className="fs-5" onClick={() => setShowScoreModal(true)}>
{t('score')}: {score as number}
</Button>
</div>
)}
</Card.Header>
<Card.Body>
<h2 className="text-dark fw-bold h6 mb-3">👥 {t('team_members')}</h2>
<ul className="list-unstyled">
{members.map(({ id, person, githubAccount }) => (
<li key={id as string} className="d-flex gap-3 align-items-center">
{/* @ts-expect-error Upstream compatibility */}
<Avatar src={(person as TableCellUser).avatar_url} />
<div>
<h3 className="fs-6 m-0 fw-bold">{(person as TableCellUser).name}</h3>

{githubAccount && (
<a
className="text-muted small"
target="_blank"
rel="noreferrer"
href={`https://github.com/${githubAccount}`}
>
@{githubAccount as string}
</a>
)}
</div>
</li>
))}
</ul>
</Card.Body>
</Card>
</Col>
<Col xs={12} sm={8}>
<Tabs variant="pills" defaultActiveKey="update" id="project-detail-tabs">
<Tab className="pt-2" eventKey="update" title={t('latest_news')}>
<div className="h1 my-5 text-center text-muted">{t('no_news_yet')}</div>
</Tab>
<Tab eventKey="teamWork" title={t('team_works')} className="pt-2">
{products && products.length > 0 ? (
<Row as="ul" className="list-unstyled g-3" xs={2}>
{products.map(product => (
<Col as="li" key={product.id as string}>
<ProductCard {...product} />
</Col>
))}
</Row>
) : (
<div className="text-center text-muted my-5">{t('no_news_yet')}</div>
)}
</Tab>
</Tabs>
</Col>
</Row>

<Card className="my-4" body>
<h3 className="h5">{t('created_by')}</h3>
<div className="mt-3">
<div className="fw-bold">{(createdBy as TableCellUser).name}</div>
<a href={`mailto:${(createdBy as TableCellUser).email}`} className="text-muted">
{(createdBy as TableCellUser).email}
</a>
</div>
</Card>

<CommentBox />

<Modal size="lg" centered show={showScoreModal} onHide={() => setShowScoreModal(false)}>
<Modal.Header closeButton>
<Modal.Title>{t('score')}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Ratio aspectRatio="16x9">
<iframe
className="w-100 h-100 border-0"
title={t('score')}
src={Object.values(formLinkMap.Evaluation)[0]}
/>
</Ratio>
</Modal.Body>
</Modal>
</Container>
);
});

export default ProjectPage;
Loading
Loading