Source: scrape.js

import logger from '../utils/logger.js';

main();

/**
 * Application Entry Point
 * 
 * This is the main entry function that initializes and runs the entire aplication.
 * It serves as the starting point for all aplication logic.
 * 
 * @function main
 * @entrypoint
 * @async 
 * @returns {void}
 */
async function main() {
    try {
        await initOptions();

        let data = await getUcilista();

        let ucilista = data;

        initButtons(ucilista);

        document.getElementById("VrstaUcilista").addEventListener("change", () => {
            vrstaFilter(ucilista);
        });

        console.log('App ready.');
    } catch (error) {
        console.error('Failed to start app:', error);
    }
}

/**
 * Filters options according to vrsta selected.
 * 
 * @param {object} ucilista - Ucilista information object.
 * @see {@link ../data/ucilista.json} for the exact structure.
 */
function vrstaFilter(ucilista) {
    let vrsta = document.getElementById("VrstaUcilista").value;

    logger.line("=", 10);
    console.log("Selected vrsta: ", vrsta);

    let selectUcilista = document.getElementById("Ucilista");
    let selectSastavnice = document.getElementById("Sastavnice");

    clear_options(selectUcilista, selectSastavnice);
    
    add_options(selectUcilista, selectSastavnice, ucilista, vrsta);
}

function initButtons(ucilista) {
    document.getElementById("search-btn").addEventListener("click", () => {
        payload.page = 1;           // reset page so that every search starts from page 1
        scrapePrograms(ucilista);
    });

    document.querySelectorAll(".next-btn").forEach(button => {
        button.addEventListener("click", () => {
            next_page(ucilista)
        });
    });

    document.querySelectorAll(".previous-btn").forEach(button => {
        button.addEventListener("click", () => {
            previous_page(ucilista)
        });
    });

    document.getElementById("close-btn").addEventListener("click", () => {
        document.getElementById("widget-container").classList.add("hidden");
    });
}

/**
 * Add default options to the select elements.
 * @param {HTMLSelectElement} selectUcilista        - The select element for ucilista 
 * @param {HTMLSelectElement} selectSastavnice      - The select element for sastavnice ucilista
 */
function add_default_options(selectUcilista, selectSastavnice) {
    console.log("Adding default options...");
    let option = new Option('Sva visoka učilišta', "-1");

    option.setAttribute("selected", "selected");
    selectUcilista.add(option);

    option = new Option('Sve sastavnice', "-1");
    option.setAttribute("selected", "selected");
    selectSastavnice.add(option);
}

/**
 * Filters the ucilista object by vrsta ucilista.   
 * @param {number} vrsta    - vrsta ucilista
 * @param {object} ucilista - An object containing ucilista and their vrsta alongside
 * other info.
 * @returns {object} - ucilista filtered by vrsta
 */
function filter_by_vrsta(vrsta, ucilista) {
    // If vrsta == -1 (default) show all options, otherwise filter by vsta
    if (vrsta == -1) {
        console.log("Adding all options...")
        return ucilista;
    } else {
        console.log("Adding only options of the vrsta: ", vrsta);
        return  Object.fromEntries(
            Object.entries(ucilista).filter(([id, uciliste]) => uciliste.vrsta == vrsta)
        )
    }
}

/**
 * Add options, filter by vrsta (-1 is all), to the Ucilista and Sastavnice select elements.
 * @param {HTMLSelectElement} selectUcilista    - The select element for ucilista. 
 * @param {HTMLSelectElement} selectSastavnice  - The select element for sastavnice.
 * @param {object} ucilista          - An json object containing all of the ucilista, their ids,
 *                                  names, and the same info for all of their sastavnice
 * @param {number} vrsta             - Vrsta ucilista, -1 is the default (all options), look at the first
 *                                  select element in scrape.html for other options and their values.
 */
function add_options(selectUcilista, selectSastavnice, ucilista, vrsta) {
    console.log("Adding options...");
    add_default_options(selectUcilista, selectSastavnice);

    let ucilista_vrsta = filter_by_vrsta(vrsta, ucilista);

    let option, id, name = null;
    let uciliste_index, sastavnica_index = 1;
    for (let uciliste in ucilista_vrsta) {
        logger.line();
        console.log("Adding options for:", ucilista_vrsta[uciliste]);
        name = ucilista_vrsta[uciliste].name;
        console.log("Name and id: ", name, uciliste);

        option = new Option(name, uciliste);
        selectUcilista.add(option, uciliste_index++);

        let sastavnice = ucilista_vrsta[uciliste].sastavnice;
        for (let sastavnica in sastavnice) {
            console.log("Adding:", sastavnice[sastavnica]);
            id = sastavnice[sastavnica].id;
            name = sastavnice[sastavnica].name;
            console.log("Sastavnica name and id:", name, id)

            option = new Option(name, id);
            selectSastavnice.add(option, sastavnica_index++);
        }
        logger.line();
    }

    console.log("Options addedd...");
}

/**
 * Clears options from select elements for ucilista and sastavnice.
 * @param {HTMLSelectElement} selectUcilista    - Select element for ucilista.
 * @param {HTMLSelectElement} selectSastavnice  - Select element for sastavnice.
 */
function clear_options(selectUcilista, selectSastavnice) {
    // clear select element
    selectUcilista.innerHTML = "";
    selectSastavnice.innerHTML = "";
}

async function getUcilista() {
    console.log("Fetching ucilista.json...");

    let ucilista = null;
    try {
        const response = await fetch("./data/ucilista.json");

        if (!response.ok) {
            throw new Error(`HTTP error: ${response.status}`);
        }

        const data = await response.json();

        return data;
    } catch (error) {
        console.error(error);
        throw new Error(error);
    }
}

const url = "https://www.postani-student.hr/webservices/Pretraga.svc/PretraziPrograme";
const proxy = 'https://corsproxy.io/?url=';

/**
 * Payload for scrapping postani-student.hr
 * @type {Object}
 * @property {string} Mjesto
 * @property {Array} lista
 * @property {number} page
 * @property {string} podrucje
 * @property {string} polje
 * @property {string} posebnaKvota
 * @property {string} search
 * @property {string} searchVisokaUcilista
 * @property {Boolean} usporedba
 */ 
let payload = {
    Mjesto: "Sva mjesta",
    lista: [],
    page: 1,
    podrucje: "-1",
    polje: "-1",
    posebnaKvota: "-1",
    search: "",
    searchVisokaUcilista: "",
    usporedba: true
}

const headers = {
    'Content-Type': 'application/json; charset=UTF-8',
    'Accept': 'application/json, text/javascript, */*; q=0.01',
    'X-Requested-With': 'XMLHttpRequest',
    'Referer': 'https://www.postani-student.hr/Ucilista/Nositelji.aspx',
    'Origin': 'https://www.postani-student.hr',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
};

function compare_to_original(lista) {
    let original_lista = ["-1","67","71","70","35","249","56","261","51","275","206","10","31","38","59","251","11","286","256","84","205","232","54","290","291","16","37","73","39","108","269","242","103","53","52","278","240","105","63","267","272","89","243","271","293","270","225","83","237","230","216","106","13","28","66","23","44","50","107","32","61","14","65","77","148","47","69","80","36","263","95","109","9","268","210","274","81","29","96","110","265","78","64","294","72","85","262","183","181","21","215","192","184","185","20","186","187","193","255","231","190","180","22","188","189","247","220","194","195","196","191","207","197","254","198","201","199","202","200","203","34","40","182","246","82","93","97","58","12","57","98","62","75","68","112","264","218","99","100","288","25","287","125","223","289","48","49","258","30","239","260","116","113","224","94","114","42","101","117","118","119","292","213","280","136","277","281","144","146","282","147","120","279","122","124","142","126","115","150","152"];
    
    const originalIds = new Set(original_lista);
    const ids = new Set(lista);

    const missing = [...originalIds].filter(id => !ids.has(id));

    const extra = [...ids].filter(id => !originalIds.has(id));

    console.log("Missing: ", missing);
    console.log("Extra: ", extra);
}

/**
 * Function that makes the lista of all the sastavnica ids that
 * should go into the payload.
 * @param {*} ucilista  - an object containing all of their ids, vrsta and names, as well
 * as their sastavnica alongside their ids and names
 * @returns {Array}     - an array of the (filtered) ids that go into the payloads lista 
 */
function make_payload_lista(ucilista) {
    let lista = [];
    let sastavnica_value = document.getElementById("Sastavnice").value;
    // check if sastavnica was selected
    if (sastavnica_value !== "-1") {
        console.log("Sastavnica: ", sastavnica_value, " selected.")
        return lista = [sastavnica_value];
    } 

    // Sve sastavnice and reset the payload
    lista = ["-1"];
    console.log('Sve sastavnice selected.');

    let ucilista_value = document.getElementById("Ucilista").value

    // if uciliste selected, list only it's sasatavnice
    if (ucilista_value !== "-1") {
        let uciliste = ucilista[ucilista_value];
        console.log("Selected: ", uciliste.name);
        for (let sastavnica in uciliste.sastavnice) {
            lista[parseInt(sastavnica) + 1] = uciliste.sastavnice[sastavnica].id;
        }
        return lista;
    }

    console.log("Sva ucilista selected.")

    // check if vrsta was set
    let vrsta = document.getElementById("VrstaUcilista").value
    let ucilista_vrsta = null;
    if (vrsta == "-1") {
        console.log("Sve vrste selected.");
        ucilista_vrsta = ucilista;
    } else {
        ucilista_vrsta = Object.fromEntries(
            Object.entries(ucilista).filter(([id, uciliste]) => uciliste.vrsta == vrsta)
        )
    }

    let i = 1;
    for (let uciliste in ucilista_vrsta) {
        let sastavnice = ucilista_vrsta[uciliste].sastavnice;
        for (let sastavnica in sastavnice) {
            lista[i++] = sastavnice[sastavnica].id;
        }
    }

    return lista;
}

function clean_programi() {
    let table = document.getElementById("programi-table");
    let tbody = table.querySelector("tbody");
    let rows = tbody.querySelectorAll("tr");

    rows.forEach((row) => {
        row.remove();
    })
}

const widget_url = 'https://www.postani-student.hr/usercontrols/uvjeticontainer.aspx?id=';

function display_widget(id) {
    document.querySelector("iframe").srcDoc = "";

    const url = widget_url + id;

    axios.get(url)
    .then(response => {
        console.log(response.data); // Logs the fetched data
        document.querySelector("iframe").srcdoc = response.data;
    
        document.getElementById("widget-container").classList.remove("hidden");
    })
    .catch(error => {
    console.error('Error fetching data:', error.message);
    });
}

function scrollToWidget() {
    const iframe = document.querySelector("iframe");
    iframe.scrollIntoView({
        behavior: 'instant',
        block: 'center',
        inline: 'center'
    });
}

/**
 * Popuplates the programi table with scraped programi data for a single page.
 * @param {*} data - the (`response.data.d) object from the response to the axios post request.
 */
function populate_programi(data) {
    let table = document.getElementById("programi-table");
    let tbody = table.querySelector("tbody");
    let programi = data.Programi;

    for (let program in programi) {
        let new_row = tbody.insertRow(-1);

        let cell = new_row.insertCell(0);
        cell.innerHTML = programi[program].naziv;
        cell.addEventListener("click", () => {
            display_widget(programi[program].idPrograma);

            setTimeout(() => {
                scrollToWidget();
            }, 100);
        });

        cell = new_row.insertCell(1);
        cell.innerHTML = programi[program].mjesto;

        cell = new_row.insertCell(2);
        cell.innerHTML = programi[program].programi;
    }
}

/**
 * Updates the payload to select the next page, that is next batch of programs.
 */
function next_page(ucilista) {
    payload.page++;
    scrapePrograms(ucilista);
}

function previous_page(ucilista) {
    payload.page--;
    scrapePrograms(ucilista);
}

function show_next_button() {
    document.querySelectorAll(".next-btn").forEach(button => {
        button.classList.remove("hidden");
    });
}

function hide_next_button() {
    document.querySelectorAll(".next-btn").forEach(button => {
        button.classList.add("hidden");
    });
}

// having a previous and a next button that are almost identical, seems redundant
function show_previous_button() {
    document.querySelectorAll(".previous-btn").forEach(button => {
        button.classList.remove("hidden");
    });
}

function hide_previous_button() {
    document.querySelectorAll(".previous-btn").forEach(button => {
        button.classList.add("hidden");
    });
}


function pager(data) {
    document.getElementById("page-number").innerHTML = "Trenutna stranica:" +
        data.CurrentPage + " od " + data.TotalPages;

    if (data.CurrentPage == 1) {
        hide_previous_button();
        data.CurrentPage === data.TotalPages ? hide_next_button() : show_next_button();
    } else if (data.CurrentPage < data.TotalPages) {
        show_next_button();
        show_previous_button();
    } else {
        hide_next_button();
        show_previous_button();
    }

}

/**
 * Scrape programs data from postani-student.hr using axios.
 */
async function scrapePrograms(ucilista) {;
    try {
        payload.lista = make_payload_lista(ucilista);
        console.log("ID list: ", payload.lista);

        let mjesto = document.getElementById("Mjesto").value;
        payload.Mjesto = (mjesto == "-1")? "Sva mjesta" : mjesto;

        console.log("Payload generated: ", payload)
        axios.post(proxy + url, payload)
        .then((response) => {
            console.log("First request!")
            console.log(response.data);

            let data = response.data.d;
            clean_programi();
            populate_programi(data);
            document.getElementById("programi-container").classList.remove("hidden");

            pager(data);

        })
    } catch (error) {
        console.log(error);
    }
}

async function getOptions(url) {
    console.log("Fetching options...");

    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        console.log('Options fetched.');
        return data;
    } catch (error) {
        console.error('Fetching failed:', error);
        throw error;
    }
}

async function initOptions() {
    const URL = "../../public/options.json";
    
    try {
        const options = await getOptions(URL);
        logger.line();
        console.log("Options", options);
        logger.line();

        const selectElements = document.querySelectorAll("select");
        console.log(selectElements);

        let index = 0;
        for (let select in options) {
            let selectElement = selectElements[index++];
            for (let option in options[select]) {
                let _option = new Option(option, options[select][option]);

                selectElement.add(_option);
                if (_option.value == "-1") {
                    _option.setAttribute("selected", "selected");
                }
            }
        }

        return true;
    } catch (error) {
        console.error("Failed to initialize options:", error);
        return false;
    }
}