diff --git a/components/Activity/CommentBox.tsx b/components/Activity/CommentBox.tsx new file mode 100644 index 0000000..29735df --- /dev/null +++ b/components/Activity/CommentBox.tsx @@ -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 ( + + ); +}); diff --git a/components/Activity/ProductCard.tsx b/components/Activity/ProductCard.tsx new file mode 100644 index 0000000..630cc06 --- /dev/null +++ b/components/Activity/ProductCard.tsx @@ -0,0 +1,77 @@ +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'; +import styles from '../../styles/Hackathon.module.less'; + +export type ProductCardProps = Product & Omit; + +export const ProductCard: FC = observer( + ({ className = '', id, createdAt, name, sourceLink, link = sourceLink, summary, ...props }) => ( + + + + {(name || link) as string} + +

{summary as string}

+
+ +
+ + {sourceLink && ( +
+ + + + +
+ )} + + +
+
+ ), +); diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 76b0824..4147ae7 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -1,3 +1,4 @@ +import { textJoin } from 'mobx-i18n'; import { observer } from 'mobx-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; @@ -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') }, ], }, { diff --git a/models/Activity.ts b/models/Activity.ts index e2d8864..e5e5f5d 100644 --- a/models/Activity.ts +++ b/models/Activity.ts @@ -2,6 +2,7 @@ import { BiDataQueryOptions, BiDataTable, BiSearch, + BiTableSchema, LarkPageData, makeSimpleFilter, normalizeText, @@ -34,7 +35,9 @@ export type Activity = LarkBase & | 'liveLink' | `database${'' | 'Schema'}`, TableCellValue - >; + > & { + databaseSchema: BiTableSchema; + }; export class ActivityModel extends BiDataTable() { client = larkClient; diff --git a/models/Hackathon.ts b/models/Hackathon.ts index 63c2048..a7afb37 100644 --- a/models/Hackathon.ts +++ b/models/Hackathon.ts @@ -6,6 +6,7 @@ import { normalizeText, TableCellRelation, TableCellText, + TableCellUser, TableCellValue, TableRecord, } from 'mobx-lark'; @@ -24,7 +25,7 @@ export class AgendaModel extends BiDataTable() { return { ...meta, ...fields, - summary: normalizeText(summary as TableCellText), + summary: (summary as TableCellText[])!.map(normalizeText), }; } } @@ -140,3 +141,47 @@ export class ProjectModel extends BiDataTable() { }; } } + +export type Member = LarkBase & + Record<'summary' | 'person' | 'skills' | 'githubAccount' | 'project' | 'status', TableCellValue>; + +export class MemberModel extends BiDataTable() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ + fields: { summary, person, skills, githubAccount, ...fields }, + ...meta + }: TableRecord) { + 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() { + client = larkClient; + + queryOptions: BiDataQueryOptions = { text_field_as_array: false }; + + extractFields({ fields: { link, sourceLink, ...fields }, ...meta }: TableRecord) { + return { + ...meta, + ...fields, + link: normalizeText(link as TableCellText), + sourceLink: normalizeText(sourceLink as TableCellText), + }; + } +} diff --git a/package.json b/package.json index 4679f60..d85b44c 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,14 @@ "test": "lint-staged && tsc --noEmit" }, "dependencies": { - "@koa/router": "^15.1.1", + "@giscus/react": "^3.1.0", + "@koa/router": "^15.2.0", "@mdx-js/loader": "^3.1.1", "@mdx-js/react": "^3.1.1", "@next/mdx": "^16.1.1", "core-js": "^3.47.0", "echarts-jsx": "^0.6.0", - "file-type": "^21.2.0", + "file-type": "^21.3.0", "idea-react": "^2.0.0-rc.13", "jsonwebtoken": "^9.0.3", "koa": "^3.1.1", @@ -32,7 +33,7 @@ "mobx": "^6.15.0", "mobx-github": "^0.6.2", "mobx-i18n": "^0.7.2", - "mobx-lark": "^2.6.3", + "mobx-lark": "^2.6.4", "mobx-react": "^9.2.1", "mobx-react-helper": "^0.5.1", "mobx-restful": "^2.1.4", @@ -81,7 +82,7 @@ "lint-staged": "^16.2.7", "next-with-less": "^3.0.1", "prettier": "^3.7.4", - "prettier-plugin-css-order": "^2.1.2", + "prettier-plugin-css-order": "^2.2.0", "sass": "^1.97.1", "typescript": "~5.9.3", "typescript-eslint": "^8.51.0" diff --git a/pages/hackathon/[id].tsx b/pages/hackathon/[id].tsx index 98e6ec7..704cf29 100644 --- a/pages/hackathon/[id].tsx +++ b/pages/hackathon/[id].tsx @@ -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'; @@ -34,7 +35,6 @@ export const getServerSideProps = compose<{ id: string }>( async ({ params }) => { const activity = await new ActivityModel().getOne(params!.id); - // @ts-expect-error Upstream compatibility const { appId, tableIdMap } = activity.databaseSchema as BiTableSchema; const [people, organizations, agenda, prizes, templates, projects] = await Promise.all([ @@ -199,11 +199,18 @@ const HackathonDetail: FC = observer(({ activity, hackatho

๐Ÿ’ก {t('projects')}

- {projects.map(({ name, score, summary, createdBy, members }) => ( + {projects.map(({ id, name, score, summary, createdBy, members }) => (
-
{name as string}
+
+ + {name as string} + +
{score as number}

{summary as string}

diff --git a/pages/hackathon/[id]/team/[tid].tsx b/pages/hackathon/[id]/team/[tid].tsx new file mode 100644 index 0000000..8f6ae7b --- /dev/null +++ b/pages/hackathon/[id]/team/[tid].tsx @@ -0,0 +1,205 @@ +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'; +import styles from '../../../../styles/Hackathon.module.less'; + +export const getServerSideProps = compose>( + cache(), + errorLogger, + async ({ params }) => { + const activity = await new ActivityModel().getOne(params!.id); + + const { appId, tableIdMap } = activity.databaseSchema; + + const project = await new ProjectModel(appId, tableIdMap.Project).getOne(params!.tid); + + // Get approved members for this project + const [members, products] = await Promise.all([ + new MemberModel(appId, tableIdMap.Member).getAll({ + project: project.name as string, + status: 'approved', + }), + 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 = 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 ( + <> + + + {/* Hero Section */} +
+ + + {currentRoute.map(({ title, href }, index, { length }) => { + const isActive = index === length - 1; + + return ( + + {title} + + ); + })} + + +

{displayName as string}

+

{description as string}

+ + {score != null && ( +
+ +
+ )} +
+
+ + + {/* Team Members Section */} +
+

๐Ÿ‘ฅ {t('team_members')}

+ + {members.map(({ id, person, githubAccount }) => ( + + +
+ +
+

+ {(person as TableCellUser).name} +

+ + {githubAccount && ( + + @{githubAccount as string} + + )} +
+
+
+ + ))} +
+
+ + {/* Team Products Section */} +
+

๐Ÿ’ก {t('team_works')}

+ + {products && products.length > 0 ? ( + + {products.map(product => ( + + + + ))} + + ) : ( +
{t('no_news_yet')}
+ )} +
+ + {/* Creator Information Section */} +
+

๐Ÿ‘ค {t('created_by')}

+ +
{(createdBy as TableCellUser).name}
+ + {(createdBy as TableCellUser).email} + +
+
+ + +
+ + setShowScoreModal(false)}> + + {t('score')} + + + +