|
| 1 | +/* app/ui/header.tsx */ |
1 | 2 | 'use client';
|
2 | 3 |
|
3 |
| -import { useState, useEffect } from 'react'; |
4 |
| -import Image from 'next/image'; |
| 4 | +import type { Dispatch, SetStateAction } from 'react'; |
5 | 5 |
|
6 |
| -const profileLinks = [ |
7 |
| - { |
8 |
| - href: "https://github.com/Jacobdeanr", |
9 |
| - label: "GitHub", |
10 |
| - svgPath: "/Source_Skyboxes_NextJS/icons/github-mark-white.svg", |
11 |
| - }, |
12 |
| - { |
13 |
| - href: "https://discord.gg/grqAfezMVs", |
14 |
| - label: "Discord", |
15 |
| - svgPath: "/Source_Skyboxes_NextJS/icons/discord-symbol-white.svg", |
16 |
| - }, |
17 |
| - { |
18 |
| - href: "https://steamcommunity.com/id/Jakobi_OBrien", |
19 |
| - label: "Steam", |
20 |
| - svgPath: "/Source_Skyboxes_NextJS/icons/steam-logo.svg", |
21 |
| - }, |
22 |
| -]; |
| 6 | +import IconLink from './iconlink'; |
| 7 | +import MoreMenu from './moremenu'; |
| 8 | +import { profileLinks } from './profile-links'; |
23 | 9 |
|
24 |
| -function IconLink({ |
25 |
| - href, |
26 |
| - label, |
27 |
| - svgPath, |
28 |
| -}: { |
29 |
| - href: string; |
30 |
| - label: string; |
31 |
| - svgPath: string; |
32 |
| -}) { |
33 |
| - return ( |
34 |
| - <a |
35 |
| - href={href} |
36 |
| - aria-label={label} |
37 |
| - target="_blank" |
38 |
| - rel="noreferrer" |
39 |
| - className=" |
40 |
| - flex items-center justify-center |
41 |
| - p-2 rounded-md |
42 |
| - bg-neutral-800/70 hover:bg-neutral-800 |
43 |
| - focus:outline-none focus:ring-2 focus:ring-amber-500 |
44 |
| - " |
45 |
| - > |
46 |
| - <Image src={svgPath} alt={label} width={24} height={24} /> |
47 |
| - </a> |
48 |
| - ); |
49 |
| -} |
| 10 | +import SortSelect from './sort-select'; |
50 | 11 |
|
51 |
| -function MoreMenu() { |
52 |
| - const [open, setOpen] = useState(false); |
53 |
| - |
54 |
| - return ( |
55 |
| - <div className="relative flex-shrink-0"> |
56 |
| - <button |
57 |
| - onClick={() => setOpen(!open)} |
58 |
| - aria-label="Open links" |
59 |
| - className="sm:hidden p-2 rounded-md bg-neutral-800/70 hover:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-amber-500" |
60 |
| - > |
61 |
| - <svg viewBox="0 0 24 24" className="w-4 h-4 fill-neutral-300"> |
62 |
| - <circle cx="5" cy="12" r="2" /> |
63 |
| - <circle cx="12" cy="12" r="2" /> |
64 |
| - <circle cx="19" cy="12" r="2" /> |
65 |
| - </svg> |
66 |
| - </button> |
67 |
| - |
68 |
| - {open && ( |
69 |
| - <div className="absolute right-0 mt-2 w-40 rounded-md bg-neutral-900 shadow-lg ring-1 ring-neutral-700/60"> |
70 |
| - <nav className="flex flex-col divide-y divide-neutral-700/60"> |
71 |
| - {profileLinks.map((link) => ( |
72 |
| - <IconLink key={link.label} href={link.href} label={link.label} svgPath={link.svgPath} /> |
73 |
| - ))} |
74 |
| - </nav> |
75 |
| - </div> |
76 |
| - )} |
77 |
| - </div> |
78 |
| - ); |
79 |
| -} |
| 12 | +type SortOption = 'alpha' | 'alpha-desc' | 'published-date-desc' | 'published-date-asc'; |
80 | 13 |
|
| 14 | +/* ------------------ main header ------------------ */ |
81 | 15 | interface HeaderProps {
|
82 |
| - onChange: (opts: { |
83 |
| - sort: 'alpha' | 'alpha-desc' | 'published-date-desc' | 'published-date-asc'; |
84 |
| - query: string; |
85 |
| - }) => void; |
| 16 | + sort: SortOption; |
| 17 | + setSort: Dispatch<SetStateAction<SortOption>>; |
| 18 | + query: string; |
| 19 | + setQuery: Dispatch<SetStateAction<string>>; |
86 | 20 | }
|
87 | 21 |
|
88 |
| -export default function Header({ onChange }: HeaderProps) { |
89 |
| - const [sort, setSort] = useState<'alpha' | 'alpha-desc' | 'published-date-desc' | 'published-date-asc'>('published-date-desc'); |
90 |
| - const [query, setQuery] = useState(''); |
91 |
| - |
92 |
| - useEffect(() => onChange({ sort, query }), [sort, query, onChange]); |
| 22 | +export default function Header({ sort, setSort, query, setQuery }: HeaderProps) { |
93 | 23 |
|
94 | 24 | return (
|
95 | 25 | <header className="sticky top-0 z-40 backdrop-blur-md bg-neutral-900/70 border-b border-neutral-800/60">
|
96 | 26 | <div className="mx-auto max-w-6xl flex items-center justify-between px-4 sm:px-6 h-16">
|
97 |
| - {/* title */} |
98 |
| - <h1 className="text-lg sm:text-2xl font-bold tracking-wide whitespace-nowrap select-none"> |
99 |
| - Jacob Robbins’ <span className="font-light">Skybox Library</span> |
100 |
| - </h1> |
| 27 | + {/* title */} |
| 28 | + <h1 className="text-lg sm:text-2xl font-bold tracking-wide whitespace-nowrap select-none"> |
| 29 | + Jacob Robbins’ <span className="font-light">Skybox Library</span> |
| 30 | + </h1> |
101 | 31 |
|
102 |
| - {/* right side (search/sort/icons) */} |
| 32 | + {/* right-side cluster */} |
103 | 33 | <div className="flex items-center gap-2 sm:gap-3">
|
104 |
| - {/* search + sort: desktop only */} |
| 34 | + {/* Search + Sort: Desktop (>= sm) */} |
105 | 35 | <div className="hidden sm:flex items-center gap-3">
|
106 | 36 | <input
|
107 | 37 | value={query}
|
108 | 38 | onChange={(e) => setQuery(e.target.value)}
|
109 | 39 | placeholder="Search…"
|
110 |
| - className="w-48 rounded-md bg-neutral-800/70 px-3 py-1.5 text-sm placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-amber-500" |
| 40 | + className=" |
| 41 | + flex-auto min-w-0 max-w-[40vw] |
| 42 | + rounded-md bg-neutral-800/70 |
| 43 | + px-3 py-1.5 text-sm placeholder:text-neutral-500 |
| 44 | + focus:outline-none focus:ring-2 focus:ring-amber-500 |
| 45 | + " |
111 | 46 | />
|
112 |
| - <select |
113 |
| - value={sort} |
114 |
| - onChange={(e) => setSort(e.target.value as any)} |
115 |
| - className="rounded-md bg-neutral-800/70 px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-500" |
116 |
| - > |
117 |
| - <option value="published-date-desc">Newest</option> |
118 |
| - <option value="alpha">Name A → Z</option> |
119 |
| - <option value="alpha-desc">Name Z → A</option> |
120 |
| - <option value="published-date-asc">Oldest</option> |
121 |
| - </select> |
| 47 | + {/* SortSelect visible md and up */} |
| 48 | + <div className="hidden md:block"> |
| 49 | + <SortSelect value={sort} onChange={setSort} /> |
| 50 | + </div> |
122 | 51 | </div>
|
123 | 52 |
|
124 |
| - {/* inline icons ≥640 px */} |
125 |
| - <nav className="hidden sm:flex items-center gap-2"> |
126 |
| - {profileLinks.map((link) => ( |
127 |
| - <IconLink key={link.label} href={link.href} label={link.label} svgPath={link.svgPath} /> |
| 53 | + {/* Icons: Desktop (lg and up) */} |
| 54 | + <nav className="hidden lg:flex items-center gap-2"> |
| 55 | + {profileLinks.map((l) => ( |
| 56 | + <IconLink key={l.label} {...l} /> |
128 | 57 | ))}
|
129 | 58 | </nav>
|
130 | 59 |
|
131 |
| - {/* 3-dot menu on mobile */} |
132 |
| - <MoreMenu /> |
| 60 | + {/* More Menu Button: Mobile-only (< sm) */} |
| 61 | + {/* TODO: Consider moving Search/Sort/Profile Links into MoreMenu content for mobile */} |
| 62 | + <div className="sm:hidden"> |
| 63 | + <MoreMenu /> |
| 64 | + </div> |
133 | 65 | </div>
|
134 | 66 | </div>
|
135 | 67 | </header>
|
|
0 commit comments