@@ -6,6 +6,83 @@ import { useState } from "react";
66import { useIssues } from "@/hooks/useIssues" ;
77import { GitHubIssue } from "@/issues/types" ;
88
9+ type Difficulty = {
10+ label : string ;
11+ value : string ;
12+ } ;
13+
14+ const difficultyAliases : Record < string , string [ ] > = {
15+ "good first issue" : [
16+ "good first issue" ,
17+ "skill: good first issue" ,
18+ ] ,
19+ beginner : [
20+ "beginner" ,
21+ "skill: beginner" ,
22+ ] ,
23+ intermediate : [
24+ "intermediate" ,
25+ "skill: intermediate" ,
26+ ] ,
27+ advanced : [
28+ "advanced" ,
29+ "skill: advanced" ,
30+ ] ,
31+ } ;
32+
33+ function getDifficulty ( issue : GitHubIssue ) : Difficulty | null {
34+ const issueWithLabels = issue as GitHubIssue & {
35+ labels ?: Array < string | { name ?: string } > ;
36+ } ;
37+
38+ const labels = issueWithLabels . labels ?? [ ] ;
39+
40+ const labelNames = labels . map ( label =>
41+ ( typeof label === "string" ? label : label . name ?? "" ) . toLowerCase ( ) ,
42+ ) ;
43+
44+ for ( const [ difficulty , aliases ] of Object . entries ( difficultyAliases ) ) {
45+ const found = aliases . some ( alias =>
46+ labelNames . includes ( alias . toLowerCase ( ) ) ,
47+ ) ;
48+
49+ if ( found ) {
50+ return {
51+ label :
52+ difficulty === "good first issue"
53+ ? "Good First Issue"
54+ : difficulty . charAt ( 0 ) . toUpperCase ( ) + difficulty . slice ( 1 ) ,
55+ value : difficulty ,
56+ } ;
57+ }
58+ }
59+
60+ return null ;
61+ }
62+
63+ function getDifficultyClasses ( difficulty : string ) {
64+ switch ( difficulty . toLowerCase ( ) ) {
65+ case "good first issue" :
66+ return "border-[#A7E8C7] bg-[#F0FFF7] text-[#16804F]" ;
67+
68+ case "beginner" :
69+ return "border-[#B7D6FF] bg-[#F2F7FF] text-[#2768C7]" ;
70+
71+ case "intermediate" :
72+ return "border-[#F1D27A] bg-[#FFF9E8] text-[#B47700]" ;
73+
74+ case "advanced" :
75+ return "border-[#F2B4B4] bg-[#FFF3F3] text-[#C62828]" ;
76+
77+ default :
78+ return "border-gray-light bg-white text-gray" ;
79+ }
80+ }
81+
82+ function getRepositoryName ( repositoryUrl : string ) {
83+ return repositoryUrl . split ( "/" ) . pop ( ) ?? "unknown" ;
84+ }
85+
986export default function IssueExplorer ( ) {
1087 const [ difficulty , setDifficulty ] = useState < string > ( "" ) ;
1188 const [ sdk , setSdk ] = useState < string > ( "" ) ;
@@ -21,63 +98,223 @@ export default function IssueExplorer() {
2198 } = useIssues ( difficulty , sdk ) ;
2299
23100 return (
24- < Container >
25- < div className = "mb-10" >
26- < h1 className = "text-2xl mb-2.5 sm:text-4xl sm:mb-5" > Issue Explorer</ h1 >
27- < RichText
28- className = "text-lg max-w-full md:max-w-[800px]"
29- markdown = "Browse open issues across the Hiero SDKs by difficulty. The four levels below are the rungs of the [Issue Progression Initiative](/blog/hiero-issue-progression-initiative)."
101+ < div className = "min-h-screen bg-white font-serif text-charcoal" >
102+ { /* Hero */ }
103+ < section className = "relative overflow-hidden bg-[#24000F]" >
104+ { /* Background gradient */ }
105+ < div
106+ className = "absolute inset-0"
107+ aria-hidden = "true"
108+ style = { {
109+ background : `
110+ radial-gradient(circle at 70% 20%, rgba(217, 45, 106, 0.55), transparent 34%),
111+ radial-gradient(circle at 88% 55%, rgba(184, 26, 86, 0.85), transparent 45%),
112+ linear-gradient(110deg, #21000d 0%, #5d0b2c 38%, #b81a56 100%)
113+ ` ,
114+ } }
115+ />
116+
117+ { /* Subtle grid */ }
118+ < div
119+ className = "absolute inset-0 opacity-20"
120+ aria-hidden = "true"
121+ style = { {
122+ backgroundImage : `
123+ linear-gradient(rgba(255,255,255,0.12) 1px, transparent 1px),
124+ linear-gradient(90deg, rgba(255,255,255,0.12) 1px, transparent 1px)
125+ ` ,
126+ backgroundSize : "54px 54px" ,
127+ } }
30128 />
31- </ div >
32-
33- < div className = "flex gap-4 mb-6" >
34- < select
35- value = { difficulty }
36- onChange = { e => {
37- setDifficulty ( e . target . value ) ;
38- } } >
39- < option value = "" > All Difficulties </ option >
40- < option value = "good first issue" > Good First Issue </ option >
41- < option value = "beginner" > Beginner </ option >
42- < option value = "intermediate" > Intermediate </ option >
43- < option value = "advanced" > Advanced </ option >
44- </ select >
45-
46- < select
47- value = { sdk }
48- onChange = { e => {
49- setSdk ( e . target . value ) ;
50- } } >
51- < option value = "" > All Repos </ option >
52- < option value = "python" > Python </ option >
53- < option value = "javascript" > JavaScript </ option >
54- < option value = "go" > Go </ option >
55- < option value = "rust" > Rust </ option >
56- < option value = "java" > Java </ option >
57- < option value = "cpp" > C++ </ option >
58- < option value = "swift" > Swift </ option >
59- </ select >
60- </ div >
61-
62- { loading && < p > Loading... </ p > }
63- { typeof error === "string" && < p > { error } </ p > }
64- { ! loading && ! error && issues . length === 0 && (
65- < p > No issues found matching the selected filters. </ p >
66- ) }
67-
68- < div className = "grid grid-cols-4 gap-6 " >
69- { issues . map ( issue => (
70- < div key = { issue . id } >
71- < a href = { issue . html_url } target = "_blank" rel = "noopener noreferrer" >
72- < RichText markdown = { issue . title ?? "" } className = "line-clamp-2" />
73- </ a >
74-
75- < p className = "text-sm opacity-70 mt-2" >
76- { issue . repository_url . split ( "/" ) . pop ( ) ?? "unknown" }
77- </ p >
129+
130+ { /* Decorative network nodes */ }
131+ < div
132+ className = "absolute inset-0 opacity-30"
133+ aria-hidden = "true"
134+ >
135+ < span className = "absolute left-[29%] top-[42%] h-3 w-3 rounded-full bg-white/70 shadow-[0_0_0_12px_rgba(255,255,255,0.08)]" />
136+ < span className = "absolute left-[47%] top-[62%] h-4 w-4 rounded-full bg-white/60 shadow-[0_0_0_18px_rgba(255,255,255,0.07)]" / >
137+ < span className = "absolute left-[69%] top-[28%] h-3 w-3 rounded-full bg-white/60 shadow-[0_0_0_14px_rgba(255,255,255,0.07)]" / >
138+ < span className = "absolute right-[10%] top-[54%] h-4 w-4 rounded-full bg-white/60 shadow-[0_0_0_18px_rgba(255,255,255,0.07)]" / >
139+ < span className = "absolute right-[28%] bottom-[17%] h-3 w-3 rounded-full bg-white/60 shadow-[0_0_0_14px_rgba(255,255,255,0.07)]" / >
140+
141+ < div className = "absolute left-[29%] top-[43%] h-px w-[21%] rotate-[18deg] bg-white/30" / >
142+ < div className = "absolute left-[48%] top-[62%] h-px w-[24%] -rotate-[29deg] bg-white/30" / >
143+ < div className = "absolute left-[69%] top-[29%] h-px w-[19%] rotate-[32deg] bg-white/30" />
144+ </ div >
145+
146+ < Container >
147+ < div className = "relative z-10 flex min-h-[580px] items-center py-24 sm:min-h-[580px] lg:min-h-[580px]" >
148+ < div className = "max-w-[620px]" >
149+ < div className = "mb-7 flex items-center gap-3 text-xs font-semibold uppercase tracking-[0.22em] text-white/75" >
150+ < span className = "h-2 w-2 rounded-full bg-red-light" / >
151+ Open Source · Hiero SDKs
152+ </ div >
153+
154+ < h1 className = "max-w-[520px] text-6xl font-medium leading-[0.92] tracking-[-0.055em] text-white sm:text-7xl lg:text-[7.5rem] lg:leading-[0.88]" >
155+ Issue
156+ < br / >
157+ Explorer
158+ </ h1 >
159+
160+ < div className = "my-8 h-px w-24 bg-white" />
161+
162+ < p className = "max-w-[520px] text-lg leading-7 tracking-[-0.02em] text-white/80 sm:text-xl" >
163+ Find open issues across Hiero SDKs and start contributing
164+ </ p >
165+
166+ < div className = "mt-8 flex flex-wrap gap-3 " >
167+ < span className = "rounded-full border border-white/20 bg-white/15 px-5 py-2.5 text-sm font-medium text-white backdrop-blur-sm" >
168+ { issues . length } open issues
169+ </ span >
170+
171+ < span className = "rounded-full border border-white/20 bg-white/15 px-5 py-2.5 text-sm font-medium text-white backdrop-blur-sm" >
172+ Ready to contribute
173+ </ span >
174+ </ div >
175+ </ div >
78176 </ div >
79- ) ) }
80- </ div >
81- </ Container >
177+ </ Container >
178+ </ section >
179+
180+ { /* Filters */ }
181+ < section className = "border-b border-gray-light bg-white" >
182+ < Container >
183+ < div className = "flex flex-col gap-4 py-4 sm:flex-row sm:items-center sm:justify-between" >
184+ < div className = "flex flex-wrap gap-3" >
185+ { /* Difficulty */ }
186+ < div className = "relative" >
187+ < select
188+ value = { difficulty }
189+ onChange = { e => setDifficulty ( e . target . value ) }
190+ className = "h-10 min-w-[140px] appearance-none rounded-lg border border-gray-light bg-white px-4 pr-10 text-sm font-medium text-charcoal outline-none transition-colors hover:border-gray focus:border-red focus:ring-2 focus:ring-red-light/20"
191+ >
192+ < option value = "" > All Difficulties</ option >
193+ < option value = "good first issue" > Good First Issue</ option >
194+ < option value = "beginner" > Beginner</ option >
195+ < option value = "intermediate" > Intermediate</ option >
196+ < option value = "advanced" > Advanced</ option >
197+ </ select >
198+
199+ < span className = "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-gray" >
200+ ▾
201+ </ span >
202+ </ div >
203+
204+ { /* Repository */ }
205+ < div className = "relative" >
206+ < select
207+ value = { sdk }
208+ onChange = { e => setSdk ( e . target . value ) }
209+ className = "h-10 min-w-[110px] appearance-none rounded-lg border border-gray-light bg-white px-4 pr-10 text-sm font-medium text-charcoal outline-none transition-colors hover:border-gray focus:border-red focus:ring-2 focus:ring-red-light/20"
210+ >
211+ < option value = "" > All Repos</ option >
212+ < option value = "python" > Python</ option >
213+ < option value = "javascript" > JavaScript</ option >
214+ < option value = "go" > Go</ option >
215+ < option value = "rust" > Rust</ option >
216+ < option value = "java" > Java</ option >
217+ < option value = "cpp" > C++</ option >
218+ < option value = "swift" > Swift</ option >
219+ </ select >
220+
221+ < span className = "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-gray" >
222+ ▾
223+ </ span >
224+ </ div >
225+ </ div >
226+
227+ < div className = "w-fit rounded-lg border border-gray-light bg-gray-light/40 px-4 py-2 text-sm font-medium text-charcoal" >
228+ { issues . length } { issues . length === 1 ? "issue" : "issues" }
229+ </ div >
230+ </ div >
231+ </ Container >
232+ </ section >
233+
234+ { /* Issue list */ }
235+ < main className = "bg-[#FAFAF9]" >
236+ < Container >
237+ < div className = "py-9 sm:py-10" >
238+ { loading && (
239+ < div className = "py-20 text-center" >
240+ < p className = "text-lg text-gray" > Loading issues...</ p >
241+ </ div >
242+ ) }
243+
244+ { typeof error === "string" && (
245+ < div className = "rounded-xl border border-red/20 bg-red/5 p-6 text-red" >
246+ < p > { error } </ p >
247+ </ div >
248+ ) }
249+
250+ { ! loading && ! error && issues . length === 0 && (
251+ < div className = "py-20 text-center" >
252+ < p className = "text-lg text-gray" >
253+ No issues found matching the selected filters.
254+ </ p >
255+ </ div >
256+ ) }
257+
258+ { ! loading && ! error && issues . length > 0 && (
259+ < div className = "grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4" >
260+ { issues . map ( issue => {
261+ const issueDifficulty = getDifficulty ( issue ) ;
262+ const repository = getRepositoryName ( issue . repository_url ) ;
263+ console . log ( issue . title , issue . labels ) ;
264+ return (
265+ < a
266+ key = { issue . id }
267+ href = { issue . html_url }
268+ target = "_blank"
269+ rel = "noopener noreferrer"
270+ className = "group flex min-h-[185px] flex-col rounded-xl border border-[#E8E8E6] bg-white p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-red/30 hover:shadow-[0_8px_30px_rgba(30,30,30,0.08)]"
271+ >
272+ { /* Repository */ }
273+ < div >
274+ < span className = "inline-flex rounded-md bg-red/5 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.04em] text-red" >
275+ { repository }
276+ </ span >
277+ </ div >
278+
279+ { /* Title */ }
280+ < div className = "mt-4 flex-1" >
281+ < RichText
282+ markdown = { issue . title ?? "" }
283+ className = "line-clamp-3 text-[15px] font-medium leading-5 tracking-[-0.02em] text-charcoal"
284+ />
285+ </ div >
286+
287+ { /* Bottom row */ }
288+ < div className = "mt-5 flex items-center justify-between border-t border-gray-light pt-4" >
289+ < div >
290+ { issueDifficulty ? (
291+ < span
292+ className = { `inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] font-medium ${ getDifficultyClasses (
293+ issueDifficulty . value ,
294+ ) } `}
295+ >
296+ < span className = "h-1.5 w-1.5 rounded-full bg-current" />
297+ { issueDifficulty . label }
298+ </ span >
299+ ) : (
300+ < span className = "text-xs text-gray" >
301+ Open issue
302+ </ span >
303+ ) }
304+ </ div >
305+
306+ < span className = "text-lg text-gray transition-transform duration-200 group-hover:translate-x-1 group-hover:text-red" >
307+ →
308+ </ span >
309+ </ div >
310+ </ a >
311+ ) ;
312+ } ) }
313+ </ div >
314+ ) }
315+ </ div >
316+ </ Container >
317+ </ main >
318+ </ div >
82319 ) ;
83- }
320+ }
0 commit comments