GamesExplorer.tsx 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. 'use client';
  2. import { useMemo, useState, type ChangeEvent } from 'react';
  3. import { Search } from 'lucide-react';
  4. import type { GameListItem } from '@/types/response/store/game';
  5. import GameCard from './GameCard';
  6. type SortKey = 'latest'|'name';
  7. const SORT_OPTIONS: { value: SortKey; label: string }[] = [
  8. { value: 'latest', label: '최신순' },
  9. { value: 'name', label: '이름순' }
  10. ];
  11. export default function GamesExplorer({ games }: { games: GameListItem[] })
  12. {
  13. const [keyword, setKeyword] = useState('');
  14. const [sort, setSort] = useState<SortKey>('latest');
  15. const filtered = useMemo(() => {
  16. let list = [...games];
  17. const kw = keyword.trim().toLowerCase();
  18. if (kw) {
  19. list = list.filter(g => g.korName.toLowerCase().includes(kw) || (g.engName?.toLowerCase().includes(kw) ?? false));
  20. }
  21. if (sort === 'name') {
  22. list.sort((a, b) => a.korName.localeCompare(b.korName));
  23. } else {
  24. list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
  25. }
  26. return list;
  27. }, [games, keyword, sort]);
  28. return (
  29. <section>
  30. <header className="mb-4 flex flex-wrap items-center justify-between gap-3">
  31. <h1 className="flex items-center gap-2 text-xl font-bold">
  32. 게임
  33. <span className="rounded-full bg-[var(--bg-elevated)] px-2 py-0.5 text-sm font-semibold text-[var(--text-secondary)]">
  34. {filtered.length.toLocaleString()}
  35. </span>
  36. </h1>
  37. <div className="flex flex-wrap gap-2">
  38. <select
  39. value={sort}
  40. onChange={(e: ChangeEvent<HTMLSelectElement>) => setSort(e.target.value as SortKey)}
  41. aria-label="정렬"
  42. className="h-9 rounded-lg border border-[var(--border-default)] bg-[var(--bg-elevated)] px-3 text-sm"
  43. >
  44. {SORT_OPTIONS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
  45. </select>
  46. <form role="search" onSubmit={(e) => e.preventDefault()} className="flex gap-2">
  47. <input
  48. type="search"
  49. value={keyword}
  50. onChange={(e) => setKeyword(e.target.value)}
  51. placeholder="게임명 검색"
  52. aria-label="검색"
  53. className="h-9 min-w-[200px] rounded-lg border border-[var(--border-default)] bg-[var(--bg-elevated)] px-3 text-sm"
  54. />
  55. <button
  56. type="submit"
  57. aria-label="검색"
  58. className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[#FFD700] text-[#333] transition hover:bg-[#FFC400]"
  59. >
  60. <Search className="size-4" />
  61. </button>
  62. </form>
  63. </div>
  64. </header>
  65. {filtered.length === 0 ? (
  66. <p className="py-16 text-center text-[var(--text-muted)]">게임이 없습니다.</p>
  67. ) : (
  68. <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
  69. {filtered.map(g => <GameCard key={g.id} game={g} />)}
  70. </div>
  71. )}
  72. </section>
  73. );
  74. }