diff --git a/pages/award/index.tsx b/pages/award/index.tsx index a0e22bd..3d52948 100644 --- a/pages/award/index.tsx +++ b/pages/award/index.tsx @@ -1,16 +1,483 @@ +import { observer } from 'mobx-react'; import { cache, compose, errorLogger } from 'next-ssr-middleware'; -import { FC } from 'react'; +import { FC, useContext, useState } from 'react'; +import { Alert, Badge, Button, Card, Col, Container, Form, Row, Spinner } from 'react-bootstrap'; +import { PageHead } from '../../components/Layout/PageHead'; import { Award, AwardModel } from '../../models/Award'; +import { I18nContext } from '../../models/Translation'; + +const parseVotes = (votes: unknown) => Number(votes) || 0; + +const formatAwardDate = (createdAt: unknown, locale: string) => + createdAt + ? new Date(Number(createdAt)).toLocaleDateString(locale, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) + : ''; export const getServerSideProps = compose(cache(), errorLogger, async () => { const awards = await new AwardModel().getAll(); - return { props: { awards } }; + const sortedAwards = [...awards].sort((a, b) => parseVotes(b.votes) - parseVotes(a.votes)); + + return { props: { awards: sortedAwards } }; }); -const AwardPage: FC<{ awards: Award[] }> = ({ awards }) => { - return <>; -}; +const AwardPage: FC<{ awards: (Award & { id: string })[] }> = observer(({ awards }) => { + const { currentLanguage, t } = useContext(I18nContext); + + // Observable list state populated initially from SSR props + const [awardList, setAwardList] = useState<(Award & { id: string })[]>(awards); + + // Form states + const [nominator, setNominator] = useState(''); + const [nomineeName, setNomineeName] = useState(''); + const [nomineeDesc, setNomineeDesc] = useState(''); + const [reason, setReason] = useState(''); + const [videoUrl, setVideoUrl] = useState(''); + + // Status states + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const [votingId, setVotingId] = useState(null); + + // Refresh award list from Lark Bitable + const refreshAwards = async () => { + try { + const updated = await new AwardModel().getAll(); + const sorted = [...updated].sort( + (a, b) => parseVotes(b.votes) - parseVotes(a.votes), + ) as (Award & { id: string })[]; + setAwardList(sorted); + } catch (err: any) { + console.error('Failed to refresh awards:', err); + } + }; + + // Submit nomination + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!nominator.trim() || !nomineeName.trim() || !reason.trim()) { + setError(t('award_required_error')); + return; + } + + setLoading(true); + setError(''); + setSuccess(false); + + try { + await new AwardModel().updateOne({ + awardName: '开放协作人奖', + nomineeName: nomineeName.trim(), + nomineeDesc: nomineeDesc.trim(), + videoUrl: videoUrl.trim(), + reason: reason.trim(), + nominator: nominator.trim(), + votes: 0, + createdAt: Date.now(), + } as any); + + setSuccess(true); + setNominator(''); + setNomineeName(''); + setNomineeDesc(''); + setReason(''); + setVideoUrl(''); + + // Refresh list to show new nominee + await refreshAwards(); + } catch (err: any) { + console.error(err); + setError(err.message || t('award_submit_error')); + } finally { + setLoading(false); + } + }; + + // Upvote candidate + const handleVote = async (awardId: string, currentVotes: any) => { + if (votingId) return; + setVotingId(awardId); + try { + const parsedVotes = parseVotes(currentVotes); + + // Optimistic local update + setAwardList(prev => + prev.map(item => (item.id === awardId ? { ...item, votes: parsedVotes + 1 } : item)), + ); + + await new AwardModel().updateOne( + { + votes: parsedVotes + 1, + } as any, + awardId, + ); + + // Refresh to sync state with database + await refreshAwards(); + } catch (err: any) { + console.error(err); + setError(err.message || t('award_vote_error')); + // Revert on failure + await refreshAwards(); + } finally { + setVotingId(null); + } + }; + + return ( +
+ + + {/* Hero Banner Section */} + + + + + {t('award_badge')} + +

+ {t('open_collaborator_award')} +

+

+ {t('award_intro')} +

+
+ + +
+ + + +
+