11import { cache , compose , errorLogger } from 'next-ssr-middleware' ;
22import { FC } from 'react' ;
3+ import { observer } from 'mobx-react-lite' ;
4+ import { Card , Col , Row , Badge , Button } from 'react-bootstrap' ;
5+ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' ;
6+ import { faTrophy , faUsers , faHeart , faEye , faArrowRight } from '@fortawesome/free-solid-svg-icons' ;
7+ import Link from 'next/link' ;
38
49import { Award , AwardModel } from '../../models/Award' ;
10+ import { MainLayout } from '../../components/Layout' ;
511
612export const getServerSideProps = compose ( cache ( ) , errorLogger , async ( ) => {
7- const awards = await new AwardModel ( ) . getAll ( ) ;
13+ const awardModel = new AwardModel ( ) ;
14+ const awards = await awardModel . getAll ( ) ;
815
9- return { props : { awards } } ;
16+ // Group awards by type
17+ const awardStats = awards . reduce ( ( acc , award ) => {
18+ const awardName = award . awardName ?. toString ( ) || 'Unknown' ;
19+ if ( ! acc [ awardName ] ) {
20+ acc [ awardName ] = {
21+ name : awardName ,
22+ nominations : [ ] ,
23+ totalVotes : 0
24+ } ;
25+ }
26+ acc [ awardName ] . nominations . push ( award ) ;
27+ acc [ awardName ] . totalVotes += Number ( award . votes || 0 ) ;
28+ return acc ;
29+ } , { } as Record < string , { name : string ; nominations : Award [ ] ; totalVotes : number } > ) ;
30+
31+ const awardTypes = Object . values ( awardStats ) ;
32+
33+ return {
34+ props : {
35+ awards,
36+ awardTypes,
37+ totalNominations : awards . length
38+ }
39+ } ;
1040} ) ;
1141
12- const AwardPage : FC < { awards : Award [ ] } > = ( { awards } ) => {
13- return < > </ > ;
42+ interface Props {
43+ awards : Award [ ] ;
44+ awardTypes : { name : string ; nominations : Award [ ] ; totalVotes : number } [ ] ;
45+ totalNominations : number ;
46+ }
47+
48+ const AwardTypeCard : FC < {
49+ awardType : { name : string ; nominations : Award [ ] ; totalVotes : number } ;
50+ isOpenCollaborator ?: boolean ;
51+ } > = ( { awardType, isOpenCollaborator = false } ) => {
52+ const getAwardIcon = ( name : string ) => {
53+ if ( name . includes ( '协作' ) || name . includes ( 'Collaborator' ) ) return faUsers ;
54+ return faTrophy ;
55+ } ;
56+
57+ const getAwardColor = ( name : string ) => {
58+ if ( name . includes ( '协作' ) || name . includes ( 'Collaborator' ) ) return 'primary' ;
59+ return 'warning' ;
60+ } ;
61+
62+ const getAwardDescription = ( name : string ) => {
63+ if ( name . includes ( '协作' ) || name . includes ( 'Collaborator' ) ) {
64+ return '表彰在開源領域展現卓越協作精神與傑出貢獻的個人與團隊' ;
65+ }
66+ return '表彰在各領域展現傑出成就的個人與項目' ;
67+ } ;
68+
69+ return (
70+ < Card className = { `h-100 shadow-sm hover-shadow-lg transition-shadow ${ isOpenCollaborator ? 'border-primary' : '' } ` } >
71+ < Card . Body >
72+ < div className = "d-flex align-items-center mb-3" >
73+ < div className = { `bg-${ getAwardColor ( awardType . name ) } bg-gradient text-white rounded-circle p-3 me-3` } >
74+ < FontAwesomeIcon icon = { getAwardIcon ( awardType . name ) } size = "lg" />
75+ </ div >
76+ < div className = "flex-grow-1" >
77+ < h4 className = "card-title mb-1" > { awardType . name } </ h4 >
78+ { isOpenCollaborator && (
79+ < Badge bg = "primary" className = "mb-2" > Featured Award</ Badge >
80+ ) }
81+ </ div >
82+ </ div >
83+
84+ < p className = "card-text text-muted mb-4" >
85+ { getAwardDescription ( awardType . name ) }
86+ </ p >
87+
88+ < div className = "d-flex justify-content-between align-items-center mb-3" >
89+ < div className = "d-flex gap-3" >
90+ < div className = "text-center" >
91+ < div className = "fw-bold text-primary fs-5" > { awardType . nominations . length } </ div >
92+ < small className = "text-muted" > 推薦數</ small >
93+ </ div >
94+ < div className = "text-center" >
95+ < div className = "fw-bold text-success fs-5" > { awardType . totalVotes } </ div >
96+ < small className = "text-muted" > 總票數</ small >
97+ </ div >
98+ </ div >
99+ < div className = "text-end" >
100+ < small className = "text-muted" >
101+ 最新推薦: { awardType . nominations . length > 0
102+ ? new Date ( Math . max ( ...awardType . nominations . map ( n =>
103+ new Date ( n . createdAt ?. toString ( ) || 0 ) . getTime ( )
104+ ) ) ) . toLocaleDateString ( )
105+ : '無'
106+ }
107+ </ small >
108+ </ div >
109+ </ div >
110+
111+ { /* Recent nominations preview */ }
112+ { awardType . nominations . length > 0 && (
113+ < div className = "border-top pt-3 mb-3" >
114+ < small className = "text-muted fw-bold" > 最新推薦:</ small >
115+ < div className = "mt-2" >
116+ { awardType . nominations . slice ( 0 , 2 ) . map ( ( nomination , index ) => (
117+ < div key = { index } className = "d-flex align-items-center mb-1" >
118+ < FontAwesomeIcon icon = { faUsers } className = "text-muted me-2" size = "sm" />
119+ < span className = "small text-truncate" >
120+ { nomination . nomineeName || '未具名候選人' }
121+ </ span >
122+ { nomination . votes && Number ( nomination . votes ) > 0 && (
123+ < Badge bg = "outline-success" className = "ms-auto" >
124+ < FontAwesomeIcon icon = { faHeart } className = "me-1" size = "sm" />
125+ { nomination . votes }
126+ </ Badge >
127+ ) }
128+ </ div >
129+ ) ) }
130+ { awardType . nominations . length > 2 && (
131+ < small className = "text-muted" >
132+ 還有 { awardType . nominations . length - 2 } 位候選人...
133+ </ small >
134+ ) }
135+ </ div >
136+ </ div >
137+ ) }
138+
139+ < div className = "d-flex gap-2" >
140+ { isOpenCollaborator ? (
141+ < Link href = "/award/open-collaborator-award" passHref >
142+ < Button variant = "primary" size = "sm" className = "flex-grow-1" >
143+ < FontAwesomeIcon icon = { faEye } className = "me-2" />
144+ 查看詳情
145+ </ Button >
146+ </ Link >
147+ ) : (
148+ < Button variant = "outline-primary" size = "sm" className = "flex-grow-1" >
149+ < FontAwesomeIcon icon = { faEye } className = "me-2" />
150+ 查看詳情
151+ </ Button >
152+ ) }
153+ </ div >
154+ </ Card . Body >
155+ </ Card >
156+ ) ;
14157} ;
15158
16- export default AwardPage ;
159+ const AwardPage : FC < Props > = observer ( ( { awards, awardTypes, totalNominations } ) => {
160+ const openCollaboratorAward = awardTypes . find (
161+ type => type . name . includes ( '协作' ) || type . name . includes ( 'Collaborator' )
162+ ) ;
163+
164+ const otherAwards = awardTypes . filter (
165+ type => ! type . name . includes ( '协作' ) && ! type . name . includes ( 'Collaborator' )
166+ ) ;
167+
168+ return (
169+ < MainLayout >
170+ < div className = "container py-5" >
171+ { /* Hero Section */ }
172+ < div className = "text-center mb-5" >
173+ < div className = "bg-gradient-primary text-white py-5 px-4 rounded-3 mb-4"
174+ style = { { background : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' } } >
175+ < FontAwesomeIcon icon = { faTrophy } size = "4x" className = "mb-4 text-warning" />
176+ < h1 className = "display-4 fw-bold mb-3" > 開源市集獎項</ h1 >
177+ < p className = "lead mb-4" >
178+ 表彰開源社群中的傑出貢獻者與創新項目
179+ </ p >
180+ < div className = "d-flex justify-content-center gap-4 text-white-50" >
181+ < div >
182+ < strong > { awardTypes . length } </ strong >
183+ < br />
184+ < small > 獎項類別</ small >
185+ </ div >
186+ < div className = "vr" > </ div >
187+ < div >
188+ < strong > { totalNominations } </ strong >
189+ < br />
190+ < small > 總推薦數</ small >
191+ </ div >
192+ < div className = "vr" > </ div >
193+ < div >
194+ < strong > { awardTypes . reduce ( ( sum , type ) => sum + type . totalVotes , 0 ) } </ strong >
195+ < br />
196+ < small > 總票數</ small >
197+ </ div >
198+ </ div >
199+ </ div >
200+ </ div >
201+
202+ { /* Featured Award - Open Collaborator Award */ }
203+ { openCollaboratorAward && (
204+ < div className = "mb-5" >
205+ < div className = "d-flex align-items-center mb-3" >
206+ < h2 className = "mb-0" >
207+ < FontAwesomeIcon icon = { faUsers } className = "me-2 text-primary" />
208+ 精選獎項
209+ </ h2 >
210+ < Badge bg = "primary" className = "ms-3" > Featured</ Badge >
211+ </ div >
212+ < Row >
213+ < Col lg = { 8 } xl = { 6 } >
214+ < AwardTypeCard
215+ awardType = { openCollaboratorAward }
216+ isOpenCollaborator = { true }
217+ />
218+ </ Col >
219+ < Col lg = { 4 } xl = { 6 } className = "d-flex align-items-center" >
220+ < div className = "text-center w-100" >
221+ < div className = "mb-3" >
222+ < FontAwesomeIcon icon = { faHeart } size = "2x" className = "text-danger mb-2" />
223+ < h4 > 立即參與</ h4 >
224+ < p className = "text-muted" >
225+ 推薦您認為值得表彰的開源協作者,讓更多人看見他們的貢獻!
226+ </ p >
227+ </ div >
228+ < Link href = "/award/open-collaborator-award" passHref >
229+ < Button variant = "primary" size = "lg" >
230+ 前往推薦
231+ < FontAwesomeIcon icon = { faArrowRight } className = "ms-2" />
232+ </ Button >
233+ </ Link >
234+ </ div >
235+ </ Col >
236+ </ Row >
237+ </ div >
238+ ) }
239+
240+ { /* All Award Types */ }
241+ < div className = "mb-5" >
242+ < h3 className = "mb-4" >
243+ < FontAwesomeIcon icon = { faTrophy } className = "me-2 text-warning" />
244+ 所有獎項類別
245+ </ h3 >
246+
247+ { awardTypes . length > 0 ? (
248+ < Row xs = { 1 } md = { 2 } lg = { 3 } xl = { 4 } className = "g-4" >
249+ { awardTypes . map ( ( awardType , index ) => (
250+ < Col key = { index } >
251+ < AwardTypeCard
252+ awardType = { awardType }
253+ isOpenCollaborator = {
254+ awardType . name . includes ( '协作' ) || awardType . name . includes ( 'Collaborator' )
255+ }
256+ />
257+ </ Col >
258+ ) ) }
259+ </ Row >
260+ ) : (
261+ < Card className = "text-center py-5" >
262+ < Card . Body >
263+ < FontAwesomeIcon icon = { faTrophy } size = "3x" className = "text-muted mb-3" />
264+ < h4 className = "text-muted" > 尚無獎項</ h4 >
265+ < p className = "text-muted mb-4" >
266+ 目前還沒有任何獎項推薦,成為第一個推薦者吧!
267+ </ p >
268+ < Link href = "/award/open-collaborator-award" passHref >
269+ < Button variant = "primary" >
270+ < FontAwesomeIcon icon = { faUsers } className = "me-2" />
271+ 推薦開放協作人獎
272+ </ Button >
273+ </ Link >
274+ </ Card . Body >
275+ </ Card >
276+ ) }
277+ </ div >
278+
279+ { /* Statistics Section */ }
280+ < div className = "bg-light rounded-3 p-4 mt-5" >
281+ < Row className = "text-center" >
282+ < Col md = { 3 } className = "mb-3 mb-md-0" >
283+ < div className = "h3 text-primary mb-1" > { awardTypes . length } </ div >
284+ < div className = "text-muted" > 獎項類別</ div >
285+ </ Col >
286+ < Col md = { 3 } className = "mb-3 mb-md-0" >
287+ < div className = "h3 text-success mb-1" > { totalNominations } </ div >
288+ < div className = "text-muted" > 總推薦數</ div >
289+ </ Col >
290+ < Col md = { 3 } className = "mb-3 mb-md-0" >
291+ < div className = "h3 text-warning mb-1" >
292+ { awardTypes . reduce ( ( sum , type ) => sum + type . totalVotes , 0 ) }
293+ </ div >
294+ < div className = "text-muted" > 總票數</ div >
295+ </ Col >
296+ < Col md = { 3 } >
297+ < div className = "h3 text-info mb-1" >
298+ { new Date ( ) . getFullYear ( ) }
299+ </ div >
300+ < div className = "text-muted" > 年度獎項</ div >
301+ </ Col >
302+ </ Row >
303+ </ div >
304+ </ div >
305+ </ MainLayout >
306+ ) ;
307+ } ) ;
308+
309+ export default AwardPage ;
0 commit comments