@@ -147,44 +147,121 @@ const QUESTIONS = [
147147
148148/* ------------------------ Rich “Learn More” modal ------------------------ */
149149/** Inline styles are provided as a fallback to guarantee visibility even if CSS is missing/overridden. */
150+ /* -------------------- modal helpers -------------------- */
151+ const toList = val => {
152+ if ( ! val ) return [ ] ;
153+ if ( Array . isArray ( val ) )
154+ return val
155+ . map ( String )
156+ . map ( s => s . trim ( ) )
157+ . filter ( Boolean ) ;
158+ // Accept newline or bullet-prefixed text (•, -, *)
159+ return String ( val )
160+ . split ( / \r ? \n | [ \u2022 • ] | ^ \s * - \s + | ^ \s * \* \s + / gm)
161+ . map ( s => s . trim ( ) )
162+ . filter ( Boolean ) ;
163+ } ;
164+
165+ const firstVal = ( obj , keys ) => {
166+ for ( const k of keys ) {
167+ if ( obj && obj [ k ] != null && String ( obj [ k ] ) . trim ( ) !== '' ) return obj [ k ] ;
168+ }
169+ return null ;
170+ } ;
171+
172+ const isReactish = ad =>
173+ / \b ( r e a c t | m e r n | f r o n t e n d ) \b / i. test ( `${ ad ?. title || '' } ${ ad ?. category || '' } ` ) ;
174+
175+ const buildSections = ad => {
176+ const reqTitle = isReactish ( ad ) ? 'React.js Position Requirements:' : 'Position Requirements:' ;
177+ const groups = [
178+ { title : 'Role Responsibilities' , keys : [ 'responsibilities' , 'whatYoullDo' , 'expectations' ] } ,
179+ {
180+ title : reqTitle ,
181+ keys : [ 'requirements' , 'positionRequirements' , 'required' , 'mustHave' , 'reactRequirements' ] ,
182+ } ,
183+ {
184+ title : 'Additional Beneficial Qualifications:' ,
185+ keys : [ 'qualifications' , 'beneficialQualifications' , 'niceToHave' , 'preferred' ] ,
186+ } ,
187+ {
188+ title : 'Availability & Commitment' ,
189+ keys : [ 'commitment' , 'availability' , 'hours' , 'timeRequirements' ] ,
190+ } ,
191+ { title : 'Tech / Tools We Use' , keys : [ 'stack' , 'techStack' , 'tools' , 'technologies' ] } ,
192+ { title : 'Benefits & What You’ll Gain' , keys : [ 'benefits' , 'perks' ] } ,
193+ ] ;
194+
195+ return groups
196+ . map ( g => ( { title : g . title , items : toList ( firstVal ( ad , g . keys ) ) } ) )
197+ . filter ( sec => sec . items . length > 0 ) ;
198+ } ;
199+
150200function DescModal ( { ad, onClose } ) {
151201 if ( ! ad ) return null ;
152202
203+ // --- derive heading: "Seeking Experienced {Job Name}" ---
204+ const stripPrefix = s =>
205+ String ( s || '' )
206+ . replace ( / ^ s e e k i n g \s + e x p e r i e n c e d \s + / i, '' )
207+ . trim ( ) ;
208+ const jobName = stripPrefix ( ad . title || ad . category || 'Position' ) ;
209+ const heading = `Seeking Experienced ${ jobName } ` ;
210+
211+ // rich HTML from API (optional)
153212 const html = ad . longHtml || ad . html ;
154213
214+ // One Community blurb (can be overridden by backend via ad.aboutOneCommunity)
215+ const aboutOneCommunity =
216+ ad . aboutOneCommunity ||
217+ `One Community is a 100%-volunteer global sustainability organization. We're a software team of over 100 developers working on the 5 phases of this open source team management software: ` +
218+ `<a href="https://www.onecommunityglobal.org/highest-good-network/" target="_blank" rel="noopener noreferrer">www.onecommunityglobal.org/highest-good-network/</a>` ;
219+
220+ // build content groups (Requirements, Responsibilities, etc.)
221+ const sections = buildSections ( ad ) ;
222+
223+ // --- styles (kept inline for resilience) ---
155224 const outerStyle = {
156225 position : 'fixed' ,
157226 inset : 0 ,
158227 display : 'grid' ,
159228 placeItems : 'center' ,
160229 zIndex : 9999 ,
161230 } ;
162- const backdropStyle = {
163- position : 'absolute' ,
164- inset : 0 ,
165- background : 'rgba(0,0,0,.45)' ,
166- } ;
231+ const backdropStyle = { position : 'absolute' , inset : 0 , background : 'rgba(0,0,0,.45)' } ;
167232 const cardStyle = {
168233 position : 'relative' ,
169234 zIndex : 1 ,
170235 width : 'min(92vw, 1100px)' ,
171236 maxHeight : '80vh' ,
172237 display : 'flex' ,
173238 flexDirection : 'column' ,
174- background : 'var(--surface, #fff)' ,
175- color : 'var(--text, #111)' ,
176- border : '1px solid var(--ring, #e5e7eb)' ,
177- borderRadius : 'var(--radius, 12px)' ,
178- boxShadow : 'var(--shadow, 0 6px 16px rgba(0,0,0,.18))' ,
179- padding : 'var(--space-5, 20px)' ,
239+ background : 'var(--surface,#fff)' ,
240+ color : 'var(--text,#111)' ,
241+ border : '1px solid var(--ring,#e5e7eb)' ,
242+ borderRadius : 'var(--radius,12px)' ,
243+ boxShadow : 'var(--shadow,0 6px 16px rgba(0,0,0,.18))' ,
244+ padding : 'var(--space-5,20px)' ,
180245 } ;
181246 const bodyStyle = { flex : 1 , overflow : 'auto' , paddingRight : 8 } ;
247+ const chipRowStyle = { display : 'flex' , flexWrap : 'wrap' , gap : 8 , margin : '8px 0 12px' } ;
248+ const chipStyle = {
249+ display : 'inline-flex' ,
250+ alignItems : 'center' ,
251+ padding : '4px 10px' ,
252+ borderRadius : 999 ,
253+ border : '1px solid var(--ring,#e5e7eb)' ,
254+ background : 'var(--card,#f7f7f9)' ,
255+ fontSize : 12 ,
256+ } ;
257+ const sectionStyle = { marginBottom : 16 } ;
258+ const listStyle = { paddingLeft : '1.25rem' , margin : '8px 0 16px' } ;
182259
183260 const Section = ( { title, items } ) =>
184261 items ?. length ? (
185- < section >
262+ < section style = { sectionStyle } >
186263 < h4 > { title } </ h4 >
187- < ul >
264+ < ul style = { listStyle } >
188265 { items . map ( ( it , idx ) => (
189266 < li key = { idx } > { it } </ li >
190267 ) ) }
@@ -199,6 +276,26 @@ function DescModal({ ad, onClose }) {
199276 }
200277 } ;
201278
279+ // Meta chips under title
280+ const chips = [ ] ;
281+ if ( ad . category ) chips . push ( ad . category ) ;
282+ if ( ad . location ) chips . push ( ad . location ) ;
283+ if ( ad . timezone ) chips . push ( ad . timezone ) ;
284+ const hours = ad . commitmentHours || ad . hours ;
285+ if ( hours ) chips . push ( `${ hours } hrs/wk` ) ;
286+ if ( ad . datePosted ) {
287+ try {
288+ chips . push ( new Date ( ad . datePosted ) . toLocaleDateString ( ) ) ;
289+ } catch { }
290+ }
291+
292+ const detailsLink =
293+ ad . jobDetailsLink ||
294+ ( ad . category &&
295+ `https://www.onecommunityglobal.org/collaboration/seeking-${ (
296+ ad . category || ''
297+ ) . toLowerCase ( ) } `) ;
298+
202299 return (
203300 < div
204301 className = "modal"
@@ -207,7 +304,6 @@ function DescModal({ ad, onClose }) {
207304 aria-modal = "true"
208305 aria-labelledby = "modal-title"
209306 >
210- { /* Backdrop is operable via keyboard */ }
211307 < div
212308 className = "modal__backdrop"
213309 style = { backdropStyle }
@@ -224,36 +320,65 @@ function DescModal({ ad, onClose }) {
224320 tabIndex = { - 1 }
225321 role = "document"
226322 >
227- < header className = "modal__header modal__header--center" >
228- < h2 id = "modal-title" > { ad . title || 'Position' } </ h2 >
323+ < header className = "modal__header modal__header--stack" >
324+ < h2 id = "modal-title" className = "modal__title" >
325+ { heading }
326+ </ h2 >
327+
328+ { chips . length > 0 && (
329+ < div className = "modal__meta" aria-label = "Job meta" >
330+ { chips . map ( ( c , i ) => (
331+ < span key = { i } className = "chip" >
332+ { c }
333+ </ span >
334+ ) ) }
335+ </ div >
336+ ) }
229337 </ header >
230338
231339 < div className = "modal__body modal__body--scroll" style = { bodyStyle } >
232- { html ? (
340+ { /* One Community blurb immediately under the heading */ }
341+ < p className = "modal__about" dangerouslySetInnerHTML = { { __html : aboutOneCommunity } } />
342+
343+ { /* If backend supplies rich HTML, show it after the intro */ }
344+ { html && (
233345 < article className = "modal__article" dangerouslySetInnerHTML = { { __html : html } } />
234- ) : (
346+ ) }
347+
348+ { /* Fallback role description if no HTML */ }
349+ { ! html && ad . description && (
235350 < article className = "modal__article" >
236- { ad . description && < p > { ad . description } </ p > }
237- < Section title = "React.js Position Requirements:" items = { ad . requirements } />
238- < Section title = "Additional Beneficial Qualifications:" items = { ad . qualifications } />
239- { Array . isArray ( ad . images ) && ad . images . length > 0 && (
240- < div className = "modal__image-grid" >
241- { ad . images . map ( ( src , i ) => (
242- < img key = { i } src = { src } alt = { `${ ad . title } visual ${ i + 1 } ` } />
243- ) ) }
244- </ div >
245- ) }
351+ < h4 > About the role</ h4 >
352+ < p > { ad . description } </ p >
246353 </ article >
247354 ) }
355+
356+ { /* Auto-built sections (Requirements, Responsibilities, etc.) */ }
357+ { sections . map ( ( s , i ) => (
358+ < section key = { i } className = "modal__section" >
359+ < h4 > { s . title } </ h4 >
360+ < ul >
361+ { s . items . map ( ( it , idx ) => (
362+ < li key = { idx } > { it } </ li >
363+ ) ) }
364+ </ ul >
365+ </ section >
366+ ) ) }
367+
368+ { /* Image grid + details link (unchanged) */ }
369+ { Array . isArray ( ad . images ) && ad . images . length > 0 && (
370+ < div className = "modal__image-grid" >
371+ { ad . images . map ( ( src , i ) => (
372+ < img key = { i } src = { src } alt = { `${ ad . title || 'Position' } visual ${ i + 1 } ` } />
373+ ) ) }
374+ </ div >
375+ ) }
248376 </ div >
249377
250- < footer className = "modal__footer modal__footer--space " >
378+ < footer className = "modal__footer" >
251379 < button className = "btn btn-primary" type = "button" onClick = { onClose } >
252380 Got it
253381 </ button >
254- < button className = "modal__close-fab" type = "button" onClick = { onClose } aria-label = "Close" >
255- X
256- </ button >
257382 </ footer >
258383 </ div >
259384 </ div >
@@ -727,7 +852,9 @@ function Collaboration() {
727852 onChange = { e => onApplyChange ( e . target . value ) }
728853 aria-label = "Apply to a position"
729854 >
730- < option value = "" > Software Engineer</ option >
855+ < option value = "" disabled >
856+ Select a position
857+ </ option >
731858 { applyOptions . map ( opt => (
732859 < option key = { opt . value } value = { opt . value } >
733860 { opt . label }
@@ -904,7 +1031,9 @@ function Collaboration() {
9041031 onChange = { e => onApplyChange ( e . target . value ) }
9051032 aria-label = "Apply to a position"
9061033 >
907- < option value = "" > Software Engineer</ option >
1034+ < option value = "" disabled >
1035+ Select a position
1036+ </ option >
9081037 { applyOptions . map ( opt => (
9091038 < option key = { opt . value } value = { opt . value } >
9101039 { opt . label }
0 commit comments