11( function ( ) {
22 const CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSu9_3lyF9caXrDdlGCtO1Bg17Uhkh_L9l-REYkYVUINvrEEaVwrx1mSZ--_iKAGcJ2x8bFBzYHVU74/pub?output=csv' ;
33 const FIELDS_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSu9_3lyF9caXrDdlGCtO1Bg17Uhkh_L9l-REYkYVUINvrEEaVwrx1mSZ--_iKAGcJ2x8bFBzYHVU74/pub?gid=1583091937&single=true&output=csv' ;
4+ const CONSUMERS_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vSu9_3lyF9caXrDdlGCtO1Bg17Uhkh_L9l-REYkYVUINvrEEaVwrx1mSZ--_iKAGcJ2x8bFBzYHVU74/pub?gid=1998786437&single=true&output=csv' ;
45
5- const CONSUMERS = [
6- { id : 'google' , label : 'Google' } ,
7- { id : 'transitapp' , label : 'Transit' } ,
8- { id : 'motis' , label : 'MOTIS' } ,
9- { id : 'OpenTripPlanner' , label : 'OTP' } ,
10- { id : 'aubin' , label : 'Aubin' } ,
11- ] ;
12-
6+ let CONSUMERS = [ ] ;
137 let knownFields = [ ] ;
148
159 const ICONS = {
1610 check : `<svg class="gft-icon icon-success" viewBox="0 0 24 24"><path d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>` ,
1711 x : `<svg class="gft-icon icon-danger" viewBox="0 0 24 24"><path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/></svg>` ,
18- clock : `<svg class="gft-icon icon-warning" viewBox="0 0 24 24"><path d="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.53 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z "/></svg>` ,
12+ clock : `<svg class="gft-icon icon-warning" viewBox="0 0 24 24"><path d="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.53 2,12A10,10 0 0,1 12,2Z "/></svg>` ,
1913 partial : `<svg class="gft-icon" viewBox="0 0 24 24" fill="#FFE296"><path d="M12,2A10,10 0 0,0 12,22A10,10 0 0,0 12,2M12,4V20A8,8 0 0,1 12,4Z"/></svg>` ,
2014 planned : `<svg class="gft-icon icon-info" viewBox="0 0 24 24"><path d="M4,15V9H12V4.16L19.84,12L12,19.84V15H4Z"/></svg>` ,
21- na : `<svg class="gft-icon" viewBox="0 0 24 24" fill="#9aa0a6"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.86.64-3.57 1.71-4.93L16.93 18.29C15.57 19.36 13.86 20 12 20zm6.29-4.71L7.71 5.71C9.07 4.64 10.78 4 12 4c4. 41 0 8 3.59 8 8 0 1.86-.64 3.57 -1.71 4.93z "/></svg>`
15+ na : `<svg class="gft-icon" viewBox="0 0 24 24" fill="#9aa0a6"><path d="M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.86.64-3.57 1.71-4.93L16.93 13.5l-1. 41 1.41L4.5 3.5l1.41 -1.41 12.02 12.02z "/></svg>` ,
2216 } ;
2317
2418 function getStatusMeta ( raw ) {
7771 const tableRows = catRows . map ( f => {
7872 const cells = CONSUMERS . map ( c => {
7973 const s = f . support [ c . id ] ;
80- const m = getStatusMeta ( s . rawStatus ) ;
81- const hasDet = s . details && s . details . trim ( ) . length > 0 ;
74+ const rawStatus = s ? s . rawStatus : '' ;
75+ const details = s ? s . details : '' ;
76+ const m = getStatusMeta ( rawStatus ) ;
77+ const hasDet = details && details . trim ( ) . length > 0 ;
8278 return `<td class="gft-cell">
8379 <div class="gft-badge ${ m . cls } ">
8480 <span class="gft-icon-anchor">${ m . icon } </span>
8581 <span class="gft-label">${ m . label } </span>
86- ${ hasDet ? `<i class="gft-info-btn" onclick="showGftDetails('${ f . name . replace ( / ' / g, "\\'" ) } • ${ c . label } ', \`${ s . details . replace ( / ` / g, '\\`' ) . replace ( / \\ / g, '\\\\' ) } \`)">i</i>` : '' }
82+ ${ hasDet ? `<i class="gft-info-btn" onclick="showGftDetails('${ f . name . replace ( / ' / g, "\\'" ) } • ${ c . label } ', \`${ details . replace ( / ` / g, '\\`' ) . replace ( / \\ / g, '\\\\' ) } \`)">i</i>` : '' }
8783 </div>
8884 </td>` ;
8985 } ) . join ( '' ) ;
9086 return `<tr class="gft-row"><td class="gft-feature-name" title="${ f . name } ">${ f . name } </td>${ cells } </tr>` ;
9187 } ) . join ( '' ) ;
9288
93- el . innerHTML = `
89+ el . innerHTML = `
9490 <div class="gft-wrapper">
9591 <div class="gft-table-wrap">
9692 <table class="gft-table" style="width:100%; border-collapse:collapse; font-family:sans-serif;">
@@ -100,7 +96,7 @@ el.innerHTML = `
10096 ${ CONSUMERS . map ( c => `
10197 <th style="border:1px solid #eee; padding:8px;">
10298 <div class="gft-consumer-header" style="display:flex; flex-direction:column; align-items:center; gap:4px;">
103- <img src="/assets/ ${ c . id } .png " alt="" style="height:16px; width:auto;" onerror="this.style.display='none'">
99+ ${ c . logo ? ` <img src="${ c . logo } " alt="${ c . label } logo " style="height:16px; width:auto;" onerror="this.style.display='none'">` : '' }
104100 <span style="font-size:11px;">${ c . label } </span>
105101 </div>
106102 </th>` ) . join ( '' ) }
@@ -129,19 +125,54 @@ el.innerHTML = `
129125
130126 Promise . all ( [
131127 fetch ( FIELDS_URL ) . then ( r => r . text ( ) ) ,
132- fetch ( CSV_URL ) . then ( r => r . text ( ) )
133- ] ) . then ( ( [ fieldsText , dataText ] ) => {
128+ fetch ( CSV_URL ) . then ( r => r . text ( ) ) ,
129+ fetch ( CONSUMERS_URL ) . then ( r => r . text ( ) )
130+ ] ) . then ( ( [ fieldsText , dataText , consumersText ] ) => {
134131 const fieldRows = parseCSV ( fieldsText ) ;
135132 knownFields = fieldRows . slice ( 1 ) . map ( r => r [ 0 ] ) . filter ( name => name && name . length > 2 ) ;
136133
137134 const rows = parseCSV ( dataText ) ;
138135 const headers = rows [ 0 ] ;
136+
137+ const discoveredConsumers = [ ] ;
138+ headers . forEach ( header => {
139+ if ( header && header . endsWith ( '_use' ) ) {
140+ const id = header . replace ( '_use' , '' ) ;
141+ const defaultLabel = id . charAt ( 0 ) . toUpperCase ( ) + id . slice ( 1 ) ;
142+ discoveredConsumers . push ( { id, label : defaultLabel , logo : '' } ) ;
143+ }
144+ } ) ;
145+
146+ if ( ! discoveredConsumers || discoveredConsumers . length === 0 ) {
147+ console . warn ( "No consumers discovered from spreadsheet headers" ) ;
148+ return ;
149+ }
150+
151+ const consumerRows = parseCSV ( consumersText ) ;
152+ const cHeaders = consumerRows [ 0 ] ;
153+ const consumerIdIdx = cHeaders . indexOf ( 'consumer' ) ;
154+ const logoUrlIdx = cHeaders . indexOf ( 'logo_url' ) ;
155+ const labelIdx = cHeaders . indexOf ( 'consumer_public_label' ) ;
156+
157+ consumerRows . slice ( 1 ) . forEach ( r => {
158+ const rawId = ( r [ consumerIdIdx ] || '' ) . trim ( ) . toLowerCase ( ) ;
159+ const target = discoveredConsumers . find ( c => c . id . toLowerCase ( ) === rawId ) ;
160+ if ( ! target ) return ;
161+ if ( logoUrlIdx !== - 1 && r [ logoUrlIdx ] ) target . logo = r [ logoUrlIdx ] . trim ( ) ;
162+ if ( labelIdx !== - 1 && r [ labelIdx ] ) target . label = r [ labelIdx ] . trim ( ) ;
163+ } ) ;
164+
165+ CONSUMERS = discoveredConsumers ;
166+
139167 const data = rows . slice ( 1 ) . filter ( r => r [ 0 ] ) . map ( r => {
140168 const support = { } ;
141169 CONSUMERS . forEach ( c => {
142170 const useIdx = headers . indexOf ( c . id + '_use' ) ;
143171 const detIdx = headers . indexOf ( c . id + '_details' ) ;
144- support [ c . id ] = { rawStatus : r [ useIdx ] || '' , details : r [ detIdx ] || '' } ;
172+ support [ c . id ] = {
173+ rawStatus : useIdx !== - 1 ? r [ useIdx ] || '' : '' ,
174+ details : detIdx !== - 1 ? r [ detIdx ] || '' : ''
175+ } ;
145176 } ) ;
146177 return { name : r [ 0 ] , category : r [ 1 ] , support } ;
147178 } ) ;
@@ -151,4 +182,4 @@ el.innerHTML = `
151182 }
152183
153184 if ( typeof document$ !== 'undefined' ) document$ . subscribe ( boot ) ; else document . addEventListener ( 'DOMContentLoaded' , boot ) ;
154- } ) ( ) ;
185+ } ) ( ) ;
0 commit comments