Podium.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. 'use client';
  2. import Link from 'next/link';
  3. import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
  4. import { Crown, Medal } from 'lucide-react';
  5. import type { RankingEntry, RankingType } from '@/types/response/ranking';
  6. interface Props {
  7. top3: RankingEntry[];
  8. type: RankingType;
  9. }
  10. function formatAmount(amount: number): string {
  11. if (amount >= 100000000) {
  12. return `${(amount / 100000000).toFixed(1)}억`;
  13. }
  14. if (amount >= 10000) {
  15. return `${(amount / 10000).toFixed(1)}만`;
  16. }
  17. return amount.toLocaleString('ko-KR');
  18. }
  19. function WinnerCard({ entry, rank, type }: { entry: RankingEntry|undefined; rank: 1|2|3; type: RankingType }) {
  20. const modifier = rank === 1 ? 'first' : rank === 2 ? 'second' : 'third';
  21. const linkHref = entry && !entry.isAnonymous ? `/channel/${entry.memberID}` : '#';
  22. if (!entry) {
  23. return (
  24. <div className={`ranking-podium__winner ranking-podium__winner--${modifier} ranking-podium__winner--empty`}>
  25. <div className="ranking-podium__rank-number">{rank}</div>
  26. <Avatar className="ranking-podium__avatar">
  27. <AvatarFallback>-</AvatarFallback>
  28. </Avatar>
  29. <div className="ranking-podium__name">대기 중</div>
  30. <div className="ranking-podium__amount">-</div>
  31. </div>
  32. );
  33. }
  34. return (
  35. <div className={`ranking-podium__winner ranking-podium__winner--${modifier}`}>
  36. {rank === 1 && (
  37. <>
  38. <div className="ranking-podium__shimmer" aria-hidden="true" />
  39. <Crown className="ranking-podium__crown" size={44} aria-hidden="true" />
  40. </>
  41. )}
  42. {rank !== 1 && (
  43. <Medal className="ranking-podium__medal" size={32} aria-hidden="true" />
  44. )}
  45. <div className="ranking-podium__rank-number">{rank}</div>
  46. <Link href={linkHref} className="ranking-podium__link">
  47. <Avatar className="ranking-podium__avatar">
  48. {(entry.channelThumb ?? entry.memberThumb) ? <AvatarImage src={(entry.channelThumb ?? entry.memberThumb)!} alt={entry.memberName} /> : null}
  49. <AvatarFallback>{entry.memberName?.[0] ?? '?'}</AvatarFallback>
  50. </Avatar>
  51. <div className="ranking-podium__name">{entry.memberName}</div>
  52. {entry.channelName && entry.channelName !== entry.memberName ? (
  53. <div className="ranking-podium__channel">{entry.channelName}</div>
  54. ) : null}
  55. </Link>
  56. <div className="ranking-podium__amount">
  57. {formatAmount(entry.moneyAmount)}
  58. {type === 'donor' ? 'P' : '원'}
  59. </div>
  60. {type !== 'donor' && entry.donorCount > 0 ? (
  61. <div className="ranking-podium__meta">후원자 {entry.donorCount.toLocaleString('ko-KR')}명</div>
  62. ) : null}
  63. </div>
  64. );
  65. }
  66. export default function Podium({ top3, type }: Props) {
  67. if (top3.length === 0) {
  68. return null;
  69. }
  70. const first = top3.find((e) => e.rank === 1);
  71. const second = top3.find((e) => e.rank === 2);
  72. const third = top3.find((e) => e.rank === 3);
  73. return (
  74. <div className="ranking-podium" role="region" aria-label="상위 3위 시상대">
  75. <div className="ranking-podium__stage">
  76. <WinnerCard entry={second} rank={2} type={type} />
  77. <WinnerCard entry={first} rank={1} type={type} />
  78. <WinnerCard entry={third} rank={3} type={type} />
  79. </div>
  80. </div>
  81. );
  82. }