Salta el contingut

Desenvolupament web dinàmic frontend amb javascript
Conceptes

Sessió 6: Arrays i Emmagatzematge local

Objectiu Sessió 6

L'objectiu serà treballar l'estructura de dades array per a treballar volums grans de dades per mostrar a les interfícies. Així com usar variables emmagatzemades localment a la interfície.

Arrays

Declaració i ús d'arrays

  • Un array és un conjunt indexat d'elements
  • Un array permet emmagatzemar diversos valors utilitzant una sola variable per a tot el conjunt
  • L'índex d'un array només pot ser un valor numèric
  • El primer element d'un array té l'índex 0
  • Els valors no han de ser forçosament consecutius, podem deixar posicions buides
  • Un sol array pot contenir elements de tipus diferents, i fins i tot pot contenir altres arrays (Multidimensionals)
  • Podem usar const o let:
    • const ho farem servir normalment i mantindrà invariable el concepte d'array, permetent canviar els valors directament a la posició
    • let ho evitarem, per que al assignar un valor a una posició, ens regenera de nou l'array i podem perdre l'anterior assignació de valors. Si que haurem de fer servir let si volem copiar arrays o usar mètodes que en modifiquin els valors.

Declaració amb Array

const fruites = new Array( "taronja", "llimona", "plàtan" );

o també, assignant valors a les posicions

const fruites = new Array();
fruites[0] = "taronja";
fruites[1] = "llimona";
fruites[2] = "plàtan";

Posar el new és opcional

En el cas específic dels Arrays, la utilització de new davant d'Array() és opcional:

const array1 = new Array()
const array2 = Array()

Cas especial en utilitzar new

Si inicialitzem l'array amb un sol valor numèric, es crea un array de la longitud donada i amb cada posició buida:

const fruites = new Array( 40 ); // fruites.length == 40

Declaració amb [ ]

const fruites = [];
fruites[0] = "taronja";
fruites[1] = "llimona";
// Deixem les posicions 2 i 3 buides
fruites[4] = "plàtan";

o bé

const fruites = [ "taronja", "llimona", , , "plàtan" ];

Recomanació

A nivell de rendiment del codi, és més eficient declarar els arrays amb [ ] que no pas amb new Array. També és el que porta menys confusió al codi.

Per tant, el mètode recomanat és fer servir sempre [ ]

Per accedir a un element individual d'un array, utilitzem el seu índex:

fruites[0]; // Primer element de l'array

Assignació de valors

La manera més directa, és assignant el valor a una posició

const fruites = []; //declaració de l'array buit
fruites[4] = "plàtan"; //assignem el valor "plàtan" a la posició amb índex 4

Tindrem també la possibilitat d'afegir valors usant mètodes de l'array, com per exemple push.

Recorregut d'arrays

Si considerem el següent array:

const fruites = [ "taronja", "llimona", , , "plàtan" ];

El podem recòrrer de diferents formes:

Iteració for

Recorrem consecutivament els índexs de l'array i en llegim el seu valor, tenint en compte que el primer índex és 0. Si una posició no té un valor assignat, retornarà undefined.

Exemple de recorrer array amb for

    const fruites = [ "taronja", "llimona", , , "plàtan" ];

    <script>
        var fruites = [ "taronja", "llimona", , , "plàtan" ];
        var llista = "";
        for (let i = 0; i < fruites.length; i++)
        {
            llista += `<li>${fruites[i]}`;
        }
        document.write(llista)
    </script>

Iteració for..in

Recorrem tots els índexs de l'array, sense necessitat de saber el seu valor numèric. Només retorna aquells índex on hi ha algun valor assignat.

Exemple de recorrer array amb for..in

    const fruites = [ "taronja", "llimona", , , "plàtan" ];

    <script>
    var fruites = [ "taronja", "llimona", , , "plàtan" ];
    var llista = "";
    for (clau in fruites)
    {
        llista += `<li>índex ${clau} = ${fruites[clau]}`;
    }
    document.write(llista)
    </script>

Atenció

for..in no garanteix retornar les claus d'un array en ordre, per tant no es recomana fer-lo servir per iterar arrays. És més adequat per recòrrer objectes, tot i que funciona sobre qualsevol tipus enumerable.

Iteració for..of

Recorrem tots els valors de l'array, sense necessitat de conèixer les seves claus. Retorna els valors existents fins a la longitud de l'array, encara que no estiguin assignats.

Exemple de recorrer array amb for..in

    const fruites = [ "taronja", "llimona", , , "plàtan" ];

    <script>
        var fruites = [ "taronja", "llimona", , , "plàtan" ];
        var llista = "";
        for (valor of fruites)
        {
            llista += `<li>${valor}`;
        }
        document.write(llista)
        </script>

Consell

for..of és el mètode més recomanat per recòrrer arrays i strings. En canvi, no es pot utilitzar amb objectes.

Explicació de les diferències entre for, for..in i for..of

for..in versus for..of Loops

Iteració forEach

Recorrem les parelles de valors i claus (índexs) de l'array. A diferència dels anteriors, forEach és un mètode dels arrays i rep com a paràmetre una funció:

Exemple de recorrer array amb for..in

    const fruites = [ "taronja", "llimona", , , "plàtan" ];

    <script>
        var fruites = [ "taronja", "llimona", , , "plàtan" ];
        var llista = "";
        fruites.forEach( function(valor, clau) {
            llista += `<li>índex ${clau} = ${valor}`;
        } )
        document.write(llista)
        </script>

Propietats i mètodes

Un dels avantatges principals a l'hora de fer servir arrays és poder utilitzar els seus mètodes predefinits, ja que poden realitzar moltes funcions que ens estalviaran codi.

Algunes propietats i mètodes dels arrays:

length

Propietat que retorna el nombre d'elements (assignats o no) d'un array, que es correspon amb el nombre següent del seu índex més gran.

var fruites = [ "taronja", "llimona", "plàtan" ];     // fruites.length = 3
var fruites = [ "taronja", "llimona", , , "plàtan" ]; // fruites.length = 5

Si li assignem un valor, truncarà o expandirà (afegint elements buits) l'array a la longitud donada.

fruites.length = 1;  // fruites = [ "taronja" ]
fruites.length = 0;  // fruites = [ ]
fruites = [];        // Equivalent a l'anterior

concat()

Uneix dos o més arrays en un de sol, o afegeix valors al final d'un array. Retorna un array nou, no modifica l'array original.

Paràmetres Arrays a concatenar, separats per comes

const fruites    = [ "taronja", "llimona", "plàtan" ];
const mesFruites = [ "pera", "kiwi" ];

let resultat1  = fruites.concat( mesFruites );          // variable
let resultat2  = fruites.concat( [ "pera", "kiwi" ] );  // array
let resultat3  = fruites.concat( "pera", "kiwi" );      // valors individuals

slice()

Extreu una part determinada d'un array. Retorna un array nou, no modifica l'array original.

Paràmetres Inici: valor a partir del que es comencen a extreure dades (per defecte la posició 0) Final: valor on s'atura l'extracció de dades, però sense incloure'l (per defecte la longitud de l'array)

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.slice(1,3);

Còpia d'arrays

És important que els valors per defecte de slice seleccionin tot el contingut de l'array. Això fa que amb aquests instrucció es pugui fer una còpia dels valors d'un array.

Per defecte, si assignem un array existent a una nova variable les dues estaran apuntant al mateix array:

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ]

let fruites2 = fruites

fruites.push("coco")

// fruites =  ['taronja', 'llimona', 'plàtan', 'pera', 'kiwi', 'coco']
// fruites2 = ['taronja', 'llimona', 'plàtan', 'pera', 'kiwi', 'coco']

En canvi si utilitzem slice es copien els valors ja que retorna un nou array:

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ]

let fruites2 = fruites.slice()

fruites.push("coco")

// fruites =  ['taronja', 'llimona', 'plàtan', 'pera', 'kiwi', 'coco']
// fruites2 = ['taronja', 'llimona', 'plàtan', 'pera', 'kiwi']

També es pot aconseguir el mateix resultat amb l'operador spread:

let fruites2 = [...fruites] // És equivalent a fruites2 = fruites.slice()

Important

Això només s'aplica als valors de tipus primitius: cadenes, numèrics i booleans. Els arrays i objectes continguts dins de l'array es copien per referència.

indexOf()

Retorna la posició de la primera coincidència un element donat dins d'un array. La cerca distingeix majúscules i minúscules. Si l'element no es troba dins l'array, retorna -1.

Paràmetres Cadena que conté l'element a cercar Índex a partir del qual començar la cerca (opcional)

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.indexOf("pera");

lastIndexOf()

Retorna la posició de la darrera coincidència un element donat dins d'un array. La cerca distingeix majúscules i minúscules. Si l'element no es troba dins l'array, retorna -1.

Paràmetres Cadena que conté l'element a cercar Índex a partir del qual començar la cerca (opcional)

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.lastIndexOf("pera");

includes()

Retorna un booleà indicant si un element donat es troba dins d'un array. La cerca distingeix majúscules i minúscules.

Paràmetres Cadena que conté l'element a cercar Índex a partir del qual començar la cerca (opcional)

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.includes("llimona");

push()

Afegeix nous elements al final de l'array. Retorna la nova mida de l'array.

Paràmetres Valors a afegir separats per coma

const fruites  = [ "taronja", "llimona", "plàtan" ];
let resultat = fruites.push( "pera", "kiwi" ); // resultat = 5

Utilitat

Juntament amb pop() són mètodes per operar amb un array com una pila.

pop()

Retorna el darrer element de l'array i l'elimina, modificant la longitud de l'array. Si l'array és buit, retona undefined. No espera cap paràmetre.

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.pop(); // resultat = "kiwi"

Utilitat

Juntament amb push() són mètodes per operar amb un array com una pila.

shift()

Retorna el primer element de l'array (posició 0) i l'elimina, desplaçant la resta d'elements. Si l'array és buit, retona undefined. No espera cap paràmetre.

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.shift(); // resultat = "taronja"

Ús pràctic

Combinant push() i shift(), podem fer que un array actui com una cua. Les cues són molt útils per crear buffers de dades.

Utilitat

Juntament amb unshift() són mètodes per operar amb un array com una pila inversa.

unshift()

Afegeix nous elements a l'inici de l'array (posició 0) desplaçant la resta d'elements. Retorna la nova mida de l'array.

const fruites  = [ "taronja", "llimona", "plàtan" ];
let resultat = fruites.unshift( "pera", "kiwi" ); // resultat = 5

Utilitat

Juntament amb shift() són mètodes per operar amb un array com una pila inversa.

join()

Retorna una cadena amb tots els elements de l'array units per una cadena separadora, que per defecte és una coma. No modifica l'array original.

Paràmetres Cadena separadora, si s'omet és una coma ","

const fruites  = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
let resultat = fruites.join(";");

sort()

Ordena alfabèticament els elements d'un array. Aquest mètode modifica l'array original.

Paràmetres Opcionalment, una funció d'ordenació diferent de la per defecte

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.sort();

toSorted()

El mètode toSorted() fa la mateixa funció que sort() retornant un nou array i sense modificar l'original.

reverse()

Inverteix l'ordre de tots els elements d'un array (reversat). Aquest mètode modifica l'array original. No espera cap paràmetre.

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.reverse();

toReversed()

El mètode toReversed() fa la mateixa funció que reverse() retornant un nou array i sense modificar l'original.

splice()

Afegeix o elimina elements a una posició donada de l'array. Aquest mètode modifica l'array original. Retorna un array amb els elements eliminats.

Paràmetres Inici: índex de la posició on es faran els canvis a l'array Esborrats: número d'elements a eliminar a partir de l'inici indicat Nous elements: elements a afegir a l'array a partir del punt d'inici

Segons els paràmetres que utilitzem, es farà una inserció, un esborrat o les dues coses alhora.

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.splice(1, 2, "mandarina");

toSpliced()

El mètode toSpliced() fa la mateixa funció que splice() retornant un nou array i sense modificar l'original.

fill()

Canvia els elements d'un array per altre valor entre dues posicions donades. Aquest mètode modifica l'array original. Retorna l'array modificat.

Paràmetres Valor: element amb el que s'ompliran les posicions de l'array Inici: índex de la primera posició a omplir (per defecte 0) Final: índex de la última posició a omplir, però sense incloure-la (per defecte array.length)

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.fill("mandarina", 1, 3);

find()

Retorna el primer element de l'array que satisfà (fa retornar true) la funció de cerca passada com a paràmetre. Si cap valor de l'array satisfà la funció de cerca, retorna undefined.

Paràmetres Aquest mètode rep una funció com a paràmetre.

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];

// Funció anònima com a paràmetre
// "e" iterarà per cada element de l'array o fins que un retorni true
fruites.find( function(e) { return e.length == 4 } )

// És molt més convenient utilitzar les funcions fletxa
fruites.find( e => e.length == 4 );

Consells

Si el que ens interessa és l'índex de l'element trobat enlloc del seu valor, podem utilitzar el mètode findIndex(). Si només volem saber si algun element satisfà la funció de cerca, podem utilitzar el mètode some() que retorna un booleà.

filter()

Retorna un nou array amb els elements de l'array que satisfan (fa retornen true) la funció de cerca passada com a paràmetre. Si cap valor de l'array satisfà la funció de cerca, retorna un array buit.

Paràmetres Aquest mètode rep una funció com a paràmetre.

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.filter( e => e.length == 4 );

map()

Retorna un nou array on cada element correspon al resultat d'executar la funció passada com a paràmetre sobre cada element de l'array original.

Paràmetres Aquest mètode rep una funció com a paràmetre.

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.map( e => e.length );

reduce()

Redueix un array a un sol valor a través d'una funció reductora que s'executa per cada element de l'array original.

Paràmetres Aquest mètode rep com a paràmetres una funció i un valor inicial per l'acumulador. Ha de seguir la següent sintaxi:

( ( acumulador, valorElement ) => càlcul() , valorInicial )

Exemple Sumar la longitud de totes les cadenes d'un array

const fruites = [ "taronja", "llimona", "plàtan", "pera", "kiwi" ];
fruites.reduce( ( ac, e ) => ac += e.length, 0 );

Paràmetres addicionals

La funció passada als mètodes find(), filter(), map() i reduce() pot tenir també aquests paràmetres:

  • element: l'element de l'array que s'està processant
  • índex: l'índex de l'element que s'està processant
  • array: l'array sobre el que s'està fent el procés

També es pot passar com a paràmetre un valor alternatiu per this dins de la funció.

Arrays multidimensionals

A JavaScript no hi ha una sintaxi específica per crear arrays de més d'una dimensió. Hem de crear un array principal i a cadascuna de les seves posicions crear-hi un nou array, i així successivament fins arribar a la dimensió desitjada.

Declaració d'un array multidimensional

Com amb els arrays simples, ho podem fer amb new:

// Declarem individualment cada array intern
const fila1 = new Array( "1", "2" );
const fila2 = new Array( "3", "4" );
const fila3 = new Array( "5", "6" );

// Finalment, l'array únic per contenir-los a tots
const taula = new Array( fila1, fila2, fila3 );

O bé amb la notació compacta utilitzant [ ]

const taula = [ ["1", "2"], ["3", "4"], ["5", "6"] ];

Un cop declarat l'array, podem accedir als elements individuals utilitzant un índex compost.

element valor
taula[ 0 ][ 0 ] 1
taula[ 2 ][ 1 ] 6
taula[ 2 ][ 3 ] undefined
taula[ 3 ][ 2 ] error

Exemple Inicialitzar una taula de X * Y cel·les amb un array

const taula = [];

for ( let i = 0; i < X; i++ )
{
   taula[ i ] = [];

   for ( let j = 0; j < Y; j++ )
   {
      taula[ i ][ j ] = valor;
   }
}

Emmagatzematge local

Idea

L'objectiu d'utilització de emmagatzematge local (local storage) a una aplicació web serà per guardar dades directament al navegador de l'usuari. Aquestes dades es mantenen fins que l'usuari les esborri manualment o el navegador les elimini. Això encaixa amb el concepte de frontend.

Si enviem, desem i recuperem dades al servidor, per exemple a una bdd, això serà backend. Ho veurem al 3a part del curs amb el llenguatge PHP.

El local storage no són cookies!.

Les cookies es desen al costat del client i s'envien sempre al servidor per a que les processi.

Diferències principals entre localStorage i cookies

Característica localStorage Cookies
Persistència Permanent (fins que l'usuari l'esborri manualment) Pot tenir data de caducitat
Capacitat ~5MB per domini ~4KB per cookie
Accessible des de Només des del client (JavaScript) Client i servidor (s'envien en cada petició HTTP)
Seguretat Vulnerable a XSS (JavaScript pot llegir-ho) Pot ser més segur si es configura HttpOnly i Secure
Casos d’ús Preferències de l'usuari, estat de l'app, dades de formularis Autenticació, seguiment d'usuaris, preferències bàsiques

Exemples d'ús:

  • Guardar preferencies de l'usuari
  • Mantenir dades entre sessions
  • Guardar informació de formularis temporalment
  • Desar l'estat d'una aplicació frontend

Web Storage

L'API Web Storage permet emmagatzemar parelles de claus i valors per un domini concret i d'una forma més intuitiva que utilitzant cookies.

L'emmagatzematge d'informació es realitza a través de les propietats localStorage i sessionStorage de l'objecte window, que actuen com a interfície dels objectes Storage associats a un domini.

Les dades de l'emmagatzematge local són específiques al protocol del document. Així, les dades que es guarden per un domini carregat amb HTTP seran diferents de les guardades pel mateix domini carregat amb HTTPS.

A diferència de les cookies, la informació de l'emmagatzematge local NO s'envia al servidor web en fer peticions HTTP o HTTPS. Si necessitem disposar de les dades al back-end, cal que les enviem explícitament.

Local Storage i Session Storage

Les dades que volem conservar es poden emmagatzemar utilitzant qualsevol de les dues propietats:

  • Amb localStorage les dades es guardaran indefinidament fins que siguin esborrades explícitament.
  • Amb sessionStorage les dades es guardaran només mentre la pestanya actual del navegador es mantingui oberta.

Múltiples sessions

Si obrim diverses pestanyes o finestres amb la mateixa URL, es crea un sessionStorage diferent per cada pestanya o finestra. En canvi, si dupliquem una pestanya es copia també el contingut del sessionStorage a la nova pestanya.

Capacitat d'emmagatzematge

5MB + 5MB L'espai d'emmagatzematge per cada domini està limitat a 5MB per cada tipus, de manera que es reparteixen un total de 10MB per cada domini, en contraposició als 4KB que pot emmagatzemar com a màxim una cookie.

localStorage sessionStorage cookies
mida màxima individual 5MB 5MB 1KB
mida màxima total 5MB * 5MB * 4KB
temps de vida indefinit sessió actual fins la data determinada
accessibilitat a les dades total només la pestanya actual total
transferència al servidor no no automàtica amb les capçaleres HTTP/HTTPS
compatibilitat HTML5 HTML5 HTML4 o superior

* Valors segons l'especificació, però cada navegador pot tenir-ne de diferents per defecte

Emmagatzematge

Es pot fer amb el mètode setItem(clau, valor) o bé utilitzant la sintaxi d'objectes assignant directament el valor a la clau com si fos una una propietat:

localStorage.setItem("usuari", "Joan");
localStorage.usuari = "Joan";
localStorage["usuari"] = "Joan";

Interfícies de l'API

Tot i que les 3 instruccions anteriors fan exactament el mateix, el mètode que es recomana és utilitzar la interfície integrada de l'API setItem(). El mateix s'aplica a la resta de mètodes d'accés.

localStorage/sessionStorage

Tots els exemples de codi seran vàlids tant per localstorage com per sessionstorage i la diferència serà només d'aplicació de la durada de l'emmagatzematge

localStorage.setItem("usuari", "Joan");
sessionStorage.setItem("usuari", "Maria");

Recuperació

Es pot fer amb el mètode getItem() o bé llegint directament el valor de la propietat que representa la clau:

nom = localStorage.getItem("usuari");
nom = localStorage.usuari;
nom = localStorage["usuari"];

Si la clau demanada no conté cap valor el mètode getItem() retorna null, però si es llegeix el valor com a propietat retorna undefined.

Esborrat

Es pot fer amb el mètode removeItem():

nom = localStorage.removeItem("usuari");

No hi ha un mètode equivalent en la notació d'objectes. Si la clau demanada no existeix, no fa res.

Format de les dades

L'emmagatzematge local només guarda cadenes de text en format UTF-16. Qualsevol altre format es converteix automàticament en un String quan s'emmagatzema.

const usuari = { nom: "Joan" };
localStorage.setItem( "dades", usuari );

localStorage.getItem( "dades" ); // ens retornarà "[object Object]"

Si volem guardar qualsevol tipus de dades complex (p.ex. arrays o objectes) primer els hem de serialitzar. Podem fer aquesta operació utilitzant els mètodes stringify() i parse() de l'obecte global estàtic JSON.

const usuari = { nom: "Joan" };
localStorage.setItem( "dades", JSON.stringify( usuari ) );

JSON.parse( localStorage.getItem( "dades" ) ); // ens retornarà { nom: "Joan" }

Exemples

Exemple Paginador d'imatges

    <!DOCTYPE html>
    <html>

    <head>
        <meta charset="UTF-8">
        <style>
            body {
                font-family: Arial, Helvetica, sans-serif;
                font-size: 1.5em;
            }

            #contenidor {
                display: flex;
                justify-content: center;
                margin-top: 25px;
                user-select: none
            }

            .fletxa {
                width: 100px;
                display: flex;
                align-items: center;
                justify-content: center;
                color: white;
                background-color: grey;
                font-size: 80px;
                line-height: 40px;
                cursor: pointer;
            }

            #galeria {
                width: 500px;
                display: flex;
                flex-wrap: wrap;
                background-color: lightgrey;
            }

            .foto {
                position: relative;
                width: 100px;
                transition: transform 0.250s;
                display: none;
                cursor: pointer;
            }

            .foto:hover {
                transform: scale(1.1);
                outline: 10px solid black;
                z-index: 1;
            }

            #comptador {
                margin-top: 1em;
                text-align: center;
            }
        </style>
    </head>

    <body>
        <div id="contenidor">
            <div id="anterior" class="fletxa">&#129080;</div>
            <div id="galeria"></div>
            <div id="seguent" class="fletxa">&#129082;</div>
        </div>
        <div id="comptador">Pàgina</div>

        <script>
            // Dades que ens arribarrien des del backend
            const fotos = [
                { id: 1, titol: "Títol de la foto 1", fitxer: "img001.jpg", exif: {} },
                { id: 2, titol: "Títol de la foto 2", fitxer: "img002.jpg", exif: {} },
                { id: 3, titol: "Títol de la foto 3", fitxer: "img003.jpg", exif: {} },
                { id: 4, titol: "Títol de la foto 4", fitxer: "img004.jpg", exif: {} },
                { id: 5, titol: "Títol de la foto 5", fitxer: "img005.jpg", exif: {} },
                { id: 6, titol: "Títol de la foto 6", fitxer: "img006.jpg", exif: {} },
                { id: 7, titol: "Títol de la foto 7", fitxer: "img007.jpg", exif: {} },
                { id: 8, titol: "Títol de la foto 8", fitxer: "img008.jpg", exif: {} },
                { id: 9, titol: "Títol de la foto 9", fitxer: "img009.jpg", exif: {} },
                { id: 10, titol: "Títol de la foto 10", fitxer: "img010.jpg", exif: {} },
                { id: 11, titol: "Títol de la foto 11", fitxer: "img011.jpg", exif: {} },
                { id: 12, titol: "Títol de la foto 12", fitxer: "img012.jpg", exif: {} },
                { id: 13, titol: "Títol de la foto 13", fitxer: "img013.jpg", exif: {} },
                { id: 14, titol: "Títol de la foto 14", fitxer: "img014.jpg", exif: {} },
                { id: 15, titol: "Títol de la foto 15", fitxer: "img015.jpg", exif: {} },
                { id: 16, titol: "Títol de la foto 16", fitxer: "img016.jpg", exif: {} },
                { id: 17, titol: "Títol de la foto 17", fitxer: "img017.jpg", exif: {} },
                { id: 18, titol: "Títol de la foto 18", fitxer: "img018.jpg", exif: {} }
            ]

            // Quantes fotos volem mostrar per pàgina
            const FOTOS_PAGINA = 5

            // Pàgina activa, inicialment la primera
            var numPagina = 1

            // Calculem quina serà la última pàgina
            var ultimaPagina = Math.ceil(fotos.length / FOTOS_PAGINA)

            // Funció curta ($) per seleccionar elements
            function $(id) {
                return document.querySelector(id)
            }

            // Crea una foto i la posa dins la galeria
            function creaFoto(foto) {
                // Calculem la pàgina que li tocaria a aquesta foto en funció del seu id
                var pagina = Math.ceil(foto.id / FOTOS_PAGINA)

                // Creem un nou element d'imatge
                var novaFoto = document.createElement("img")

                // La ruta de la imatge la trobem a partir del id de la foto
                novaFoto.src = "media/fotos/" + foto.fitxer

                // Posem una etiqueta emergent a la imatge amb el títol de la foto
                novaFoto.title = foto.titol

                // Apliquem l'estil de la classe foto i també posem una classe amb la pàgina
                novaFoto.classList.add("foto", "pag" + pagina)

                // Fem que la foto no es pugui arrossegar (només per estètica)
                novaFoto.draggable = false;

                // Inserim la foto dins la galeria com a últim element
                $("#galeria").appendChild(novaFoto)
            }

            // Fa visibles les fotos de la pàgina indicada (num)
            function mostraPagina(num) {
                // Seleccionem tots els elements de la classe "foto"
                var llista = document.getElementsByClassName("foto")
                // Per cada foto...
                for (foto of llista) {
                    // La fem visible només si té la classe corresponent a la pàgina indicada
                    foto.style.display = foto.classList.contains("pag" + num) ? "block" : "none"
                }
                // Actualitzem el comptador de pàgines
                $("#comptador").innerHTML = "Pàgina " + num + " de " + ultimaPagina
            }

            // Fem les vores dels botons de canvi de pàgina arrodonides
            $("#anterior").style["border-radius"] = "30px 0 0 30px"
            $("#seguent").style["border-radius"] = "0 30px 30px 0"

            // Quan cliquem "anterior" reculem una pàgina si no som ja a la primera
            $("#anterior").onclick = function () {
                if (numPagina > 1) mostraPagina(--numPagina)
            }

            // Quan cliquem "seguent" avancem una pàgina si no som ja a la última
            $("#seguent").onclick = function () {
                if (numPagina < ultimaPagina) mostraPagina(++numPagina)
            }

            // Creem un nou objecte d'imatge per cada foto que ens ha arribat del backend
            for (foto of fotos) creaFoto(foto)

            // Inicialitzem la galeria mostrant la pàgina activa
            mostraPagina(numPagina)
        </script>
    </body>

    </html>
    const fotos = [
        { id: 1, titol: "Títol de la foto 1", fitxer: "img001.jpg", exif: {} },
        { id: 2, titol: "Títol de la foto 2", fitxer: "img002.jpg", exif: {} },
        { id: 3, titol: "Títol de la foto 3", fitxer: "img003.jpg", exif: {} },
        { id: 4, titol: "Títol de la foto 4", fitxer: "img004.jpg", exif: {} },
        { id: 5, titol: "Títol de la foto 5", fitxer: "img005.jpg", exif: {} },
        { id: 6, titol: "Títol de la foto 6", fitxer: "img006.jpg", exif: {} },
        { id: 7, titol: "Títol de la foto 7", fitxer: "img007.jpg", exif: {} },
        { id: 8, titol: "Títol de la foto 8", fitxer: "img008.jpg", exif: {} },
        { id: 9, titol: "Títol de la foto 9", fitxer: "img009.jpg", exif: {} },
        { id: 10, titol: "Títol de la foto 10", fitxer: "img010.jpg", exif: {} },
        { id: 11, titol: "Títol de la foto 11", fitxer: "img011.jpg", exif: {} },
        { id: 12, titol: "Títol de la foto 12", fitxer: "img012.jpg", exif: {} },
        { id: 13, titol: "Títol de la foto 13", fitxer: "img013.jpg", exif: {} },
        { id: 14, titol: "Títol de la foto 14", fitxer: "img014.jpg", exif: {} },
        { id: 15, titol: "Títol de la foto 15", fitxer: "img015.jpg", exif: {} },
        { id: 16, titol: "Títol de la foto 16", fitxer: "img016.jpg", exif: {} },
        { id: 17, titol: "Títol de la foto 17", fitxer: "img017.jpg", exif: {} },
        { id: 18, titol: "Títol de la foto 18", fitxer: "img018.jpg", exif: {} }
    ]

Exemple Última visita

    <!DOCTYPE html>
    <html lang="ca">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Última Visita</title>
        <style>
            body {
                background-color: #f4f4f4;
                display: flex;
                justify-content: center;
                align-items: center;
                height: 100vh;
                margin: 0;
                font-family: Arial, sans-serif;
            }

            .contenidor {
                text-align: center;
                background: white;
                padding: 20px;
                border-radius: 10px;
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
                width: 300px;
            }

            .missatge {
                background: #e3f2fd;
                color: #1565c0;
                padding: 15px;
                border-radius: 5px;
                font-weight: bold;
                margin-top: 10px;
            }
        </style>
    </head>
    <body>

        <div class="contenidor">
            <h2>Benvingut a la pàgina!</h2>
            <div id="missatge-visita" class="missatge">
                Carregant informació de la teva última visita...
            </div>
        </div>

        <script>
            document.addEventListener("DOMContentLoaded", function() {
                const missatge = document.getElementById("missatge-visita");

                // Obtenir la última visita del localStorage
                const ultimaVisita = localStorage.getItem("ultimaVisita");

                if (ultimaVisita) {
                    missatge.textContent = `La teva última visita va ser el ${ultimaVisita}`;
                } else {
                    missatge.textContent = "Aquesta és la teva primera visita!";
                }

                // Guardar la data i hora actual al localStorage per la pròxima visita
                const ara = new Date().toLocaleString();
                localStorage.setItem("ultimaVisita", ara);
            });
        </script>

    </body>
    </html>
<script>
    document.addEventListener("DOMContentLoaded", function() {
        const missatge = document.getElementById("missatge-visita");

        // Obtenir la última visita del localStorage
        const ultimaVisita = localStorage.getItem("ultimaVisita");

        if (ultimaVisita) {
            missatge.textContent = `La teva última visita va ser el ${ultimaVisita}`;
        } else {
            missatge.textContent = "Aquesta és la teva primera visita!";
        }

        // Guardar la data i hora actual al localStorage per la pròxima visita
        const ara = new Date().toLocaleString();
        localStorage.setItem("ultimaVisita", ara);
    });
</script>