diff --git a/components/Navigator/MainNavigator.tsx b/components/Navigator/MainNavigator.tsx index 943f739..0d4bed3 100644 --- a/components/Navigator/MainNavigator.tsx +++ b/components/Navigator/MainNavigator.tsx @@ -1,4 +1,4 @@ -import { textJoin } from 'mobx-i18n'; +import { textJoin } from 'mobx-i18n'; import { observer } from 'mobx-react'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/router'; @@ -29,7 +29,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ subs: [ { href: '/article/join-us', title: t('join_us') }, { - href: '/article/open-collaborator-award', + href: '/award', title: t('open_collaborator_award'), }, { href: '/volunteer', title: t('volunteer') }, @@ -63,6 +63,7 @@ const topNavBarMenu = ({ t }: typeof i18n): MenuItem[] => [ ], }, { href: '/bounty', title: t('bounty') }, + { href: '/library', title: t('open_library') }, { title: t('NGO'), subs: [ diff --git a/pages/award/index.tsx b/pages/award/index.tsx index a0e22bd..c32891c 100644 --- a/pages/award/index.tsx +++ b/pages/award/index.tsx @@ -1,16 +1,172 @@ +import { TableCellValue } from 'mobx-lark'; +import { observer } from 'mobx-react'; import { cache, compose, errorLogger } from 'next-ssr-middleware'; -import { FC } from 'react'; +import { FC, useContext } from 'react'; +import { Badge, Card, Col, Container, Row } from 'react-bootstrap'; +const ALLOWED_EMBED_HOSTS = new Set([ + 'player.bilibili.com', + 'www.youtube.com', + 'www.youtube-nocookie.com', +]); +const getSafeEmbedUrl = (value: unknown): string | null => { + if (typeof value !== 'string' || !value) return null; + try { + const url = new URL(value); + return ALLOWED_EMBED_HOSTS.has(url.hostname) ? url.toString() : null; + } catch { return null; } +}; + +import { PageHead } from '../../components/Layout/PageHead'; +import { SectionTitle } from '../../components/Layout/SectionTitle'; import { Award, AwardModel } from '../../models/Award'; +import { I18nContext } from '../../models/Translation'; export const getServerSideProps = compose(cache(), errorLogger, async () => { const awards = await new AwardModel().getAll(); - return { props: { awards } }; }); -const AwardPage: FC<{ awards: Award[] }> = ({ awards }) => { - return <>; -}; +const NominationCard: FC<{ award: Award; index: number }> = observer(({ award, index }) => { + const { t } = useContext(I18nContext); + const name = (award.nomineeName ?? award.awardName ?? '') as string; + const desc = (award.nomineeDesc ?? award.reason ?? '') as string; + const nominator = (award.nominator ?? '') as string; + const videoUrl = getSafeEmbedUrl(award.videoUrl) ?? ''; + const votes = (award.votes ?? 0) as number; + + return ( + + {videoUrl && ( +
+