2026-09-18 18:55:58 +02:00
const state = { rows : [], mode : "machine" , suggestionIndex : - 1 };
const labels = {
oil_fuel_filters : "Olej / paliwo" ,
air_cooling_filters : "Powietrze / chłodzenie" ,
hydraulic_other_filters : "Hydrauliczny / inne"
};
const $ = ( selector ) => document . querySelector ( selector );
function parseCsv ( text ) {
const rows = [], record = [];
let field = "" , quoted = false ;
for ( let i = 0 ; i < text . length ; i += 1 ) {
const char = text [ i ], next = text [ i + 1 ];
if ( char === '"' && quoted && next === '"' ) { field += '"' ; i += 1 ; }
else if ( char === '"' ) quoted = ! quoted ;
else if ( char === "," && ! quoted ) { record . push ( field ); field = "" ; }
else if (( char === "\n" || char === "\r" ) && ! quoted ) {
if ( char === "\r" && next === "\n" ) i += 1 ;
record . push ( field ); field = "" ;
if ( record . some (( value ) => value . trim ())) rows . push ( record . splice ( 0 ));
} else field += char ;
}
record . push ( field );
if ( record . some (( value ) => value . trim ())) rows . push ( record );
const headers = rows . shift (). map (( header ) => header . replace ( /^\uFEFF/ , "" ). trim ());
return rows . map (( values ) => Object . fromEntries ( headers . map (( header , index ) => [ header , ( values [ index ] || "" ). trim ()])));
}
function normalized ( value ) {
return ( value || "" ). toLocaleLowerCase ( "pl-PL" ). normalize ( "NFD" ). replace ( /[\u0300-\u036f]/g , "" );
}
function escapeHtml ( value ) {
return String ( value || "" ). replace ( /[&<>"']/g , ( char ) => ({ "&" : "&" , "<" : "<" , ">" : ">" , '"' : """ , "'" : "'" }[ char ]));
}
function highlighted ( value , query ) {
const safe = escapeHtml ( value );
if ( ! query ) return safe ;
const escaped = query . replace ( /[.*+?^${}()|[\]\\]/g , "\\$&" );
return safe . replace ( new RegExp ( `( ${ escaped } )` , "ig" ), "<mark>$1</mark>" );
}
function allFilterText ( row ) {
return [ "oil_fuel_filters" , "air_cooling_filters" , "hydraulic_other_filters" ]. map (( key ) => row [ key ]). join ( " " );
}
function filterParts ( value ) {
const parts = value . split ( /\s*\|\s*|(?=(?:Olej|Paliwo|Powietrze|Hydrauliczny|Odpowietrznik|Płyn|Kabinowy)\b)/i )
. map (( part ) => part . replace ( /\s*\|\s*$/ , "" ). trim ()). filter ( Boolean );
return parts . length ? parts : [ value ];
}
function suggestionData ( query ) {
const needle = normalized ( query );
if ( state . mode === "machine" ) {
return [... new Set ( state . rows . map (( row ) => row . model ). filter ( Boolean ))]
. filter (( value ) => normalized ( value ). includes ( needle ))
. sort (( a , b ) => a . localeCompare ( b , "pl" ))
. slice ( 0 , 8 )
. map (( value ) => ({ value , meta : "model maszyny" }));
}
const values = new Map ();
Object . entries ( labels ). forEach (([ key , label ]) => {
state . rows . forEach (( row ) => {
const value = row [ key ];
if ( ! value ) return ;
filterParts ( value ). forEach (( part ) => {
if ( normalized ( part ). includes ( needle ) && ! values . has ( part )) values . set ( part , label );
});
});
});
return [... values . entries ()]. sort (([ a ], [ b ]) => a . localeCompare ( b , "pl" )). slice ( 0 , 8 )
. map (([ value , meta ]) => ({ value , meta }));
}
function renderSuggestions () {
const input = $ ( "#searchInput" );
const query = input . value . trim ();
const container = $ ( "#suggestions" );
state . suggestionIndex = - 1 ;
if ( query . length < 2 || ! state . rows . length ) {
container . classList . remove ( "visible" );
container . innerHTML = "" ;
return ;
}
const suggestions = suggestionData ( query );
container . innerHTML = suggestions . map (( item , index ) =>
`<button class="suggestion" type="button" role="option" data-index=" ${ index } " data-value=" ${ escapeHtml ( item . value ) } ">
<span class="suggestion-value"> ${ highlighted ( item . value , query ) } </span><span class="suggestion-meta"> ${ item . meta } </span>
</button>` ). join ( "" );
container . classList . toggle ( "visible" , suggestions . length > 0 );
container . querySelectorAll ( ".suggestion" ). forEach (( button ) => button . addEventListener ( "mousedown" , ( event ) => {
event . preventDefault ();
input . value = button . dataset . value ;
container . classList . remove ( "visible" );
$ ( "#clearButton" ). classList . add ( "visible" );
render ();
}));
}
function render () {
const query = $ ( "#searchInput" ). value . trim ();
const needle = normalized ( query );
const results = state . rows . filter (( row ) => {
const haystack = state . mode === "machine"
? [ row . model , row . machine_type , row . engine , row . year , row . notes ]. join ( " " )
: allFilterText ( row );
return ! needle || normalized ( haystack ). includes ( needle );
});
$ ( "#resultsTitle" ). textContent = state . mode === "machine" ? ( query ? "Pasujące maszyny" : "Maszyny w bazie" ) : ( query ? "Maszyny pasujące do filtra" : "Wyszukaj numer filtra" );
$ ( "#resultCount" ). textContent = query ? ` ${ results . length } wyników` : ` ${ state . rows . length } rekordów` ;
const container = $ ( "#results" );
container . innerHTML = "" ;
if ( ! results . length ) {
container . innerHTML = `<div class="empty">Nie znaleziono wyników. Spróbuj krótszego numeru filtra lub nazwy modelu.</div>` ;
return ;
}
results . forEach (( row ) => {
const card = $ ( "#machineTemplate" ). content . cloneNode ( true );
card . querySelector ( ".card-kicker" ). textContent = row . machine_type || "Maszyna" ;
card . querySelector ( ".machine-name" ). innerHTML = highlighted ( row . model , query );
card . querySelector ( ".year" ). textContent = row . year ? `Rok ${ row . year . replace ( ".0" , "" ) } ` : "" ;
card . querySelector ( ".engine" ). textContent = row . engine ? `Silnik: ${ row . engine } ` : "Silnik: brak danych" ;
const grid = card . querySelector ( ".filter-grid" );
Object . entries ( labels ). forEach (([ key , label ]) => {
if ( ! row [ key ]) return ;
const item = document . createElement ( "div" );
item . className = "filter" ;
const values = filterParts ( row [ key ])
. map (( value ) => `<span class="filter-line"> ${ highlighted ( value , query ) } </span>` )
. join ( "" );
item . innerHTML = `<span class="filter-label"> ${ label } </span><span class="filter-value"> ${ values } </span>` ;
grid . appendChild ( item );
});
card . querySelector ( ".source" ). textContent = row . source_page ? `Źródło: strona ${ row . source_page } PDF` : "" ;
container . appendChild ( card );
});
}
function setLoaded ( rows , source ) {
state . rows = rows ;
render ();
renderSuggestions ();
}
async function loadDefault () {
try {
const response = await fetch ( "atut_baza.csv" );
if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` );
setLoaded ( parseCsv ( await response . text ()), "Źródło: atut_baza.csv" );
} catch ( error ) {
2026-09-18 19:50:48 +02:00
$ ( "#results" ). innerHTML = `<div class="empty">Nie udało się wczytać bazy danych. Uruchom aplikację przez lokalny serwer.</div>` ;
2026-09-18 18:55:58 +02:00
}
}
document . querySelectorAll ( ".tab" ). forEach (( tab ) => tab . addEventListener ( "click" , () => {
document . querySelectorAll ( ".tab" ). forEach (( item ) => item . classList . remove ( "active" ));
tab . classList . add ( "active" );
state . mode = tab . dataset . mode ;
$ ( "#searchLabel" ). textContent = state . mode === "machine" ? "Model, typ lub silnik maszyny" : "Numer lub oznaczenie filtra" ;
$ ( "#searchInput" ). placeholder = state . mode === "machine" ? "np. QES 20, AMMANN albo KUBOTA" : "np. C 14 200, WK 1020X albo FF00758" ;
$ ( "#searchHint" ). textContent = state . mode === "machine" ? "Wpisz minimum 2 znaki. Wyniki aktualizują się automatycznie." : "Wpisz fragment oznaczenia, np. C 14 200 albo WK 1020X." ;
render ();
renderSuggestions ();
}));
$ ( "#searchInput" ). addEventListener ( "input" , () => { $ ( "#clearButton" ). classList . toggle ( "visible" , Boolean ( $ ( "#searchInput" ). value )); render (); renderSuggestions (); });
$ ( "#searchInput" ). addEventListener ( "keydown" , ( event ) => {
const items = [... document . querySelectorAll ( ".suggestion" )];
if ( ! items . length ) return ;
if ( event . key === "ArrowDown" || event . key === "ArrowUp" ) {
event . preventDefault ();
state . suggestionIndex = ( state . suggestionIndex + ( event . key === "ArrowDown" ? 1 : items . length - 1 )) % items . length ;
items . forEach (( item , index ) => item . classList . toggle ( "selected" , index === state . suggestionIndex ));
} else if ( event . key === "Enter" && state . suggestionIndex >= 0 ) {
event . preventDefault ();
items [ state . suggestionIndex ]. dispatchEvent ( new MouseEvent ( "mousedown" , { bubbles : true }));
} else if ( event . key === "Escape" ) {
$ ( "#suggestions" ). classList . remove ( "visible" );
}
});
$ ( "#searchInput" ). addEventListener ( "focus" , renderSuggestions );
$ ( "#searchInput" ). addEventListener ( "blur" , () => setTimeout (() => $ ( "#suggestions" ). classList . remove ( "visible" ), 120 ));
$ ( "#clearButton" ). addEventListener ( "click" , () => { $ ( "#searchInput" ). value = "" ; $ ( "#clearButton" ). classList . remove ( "visible" ); render (); renderSuggestions (); $ ( "#searchInput" ). focus (); });
loadDefault ();