Septième et dernier article sur LoFiDroX: les composants svelte.

Composant Svelte: structure et fonctionnement

Un composant Svelte est divisé en 3 parties obligatoires (qui peuvent rester vides):

  • style: on y définit les styles CSS, soit directement soit en les incluant par @import.
  • script: dans cette section, on ajoute toute la logique de l’écran.
  • enfin on ajoute le code HTML qui sera affiché par le composant.

Note: les composants sont autonomes et n’ont pas connaissance des états des autres composants.

Composant cmpLfdError.svelte

Ce composant affiche une page d’erreur et permet de revenir à la page de login.

    
 1<style>
 2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
 3    @import "../css/lofidrox.css";
 4</style>
 5
 6<script>
 7	import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 8	import LfdUrls from '../js/lfdUrlManager.js';
 9
10	function goLogin() {
11        GtgUtilsWeb.route(LfdUrls.spa_user_login);
12    };
13</script>
14
15<main class="container">
16	<section class="h-100 lfd-gradient-form">
17		<div class="container py-2 h-100">
18			<div class="row d-flex justify-content-center align-items-center h-100">
19				<div class="col-xl-10">
20					<div class="card rounded-3 text-black">
21						<div class="row g-0">
22							<div class="col-md-6">
23								<div class="card-body p-md-5 mx-md-4">
24									<div class="text-center">
25										<img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
26										<h4 class="mt-1 mb-5 pb-1"></h4>
27									</div>
28									<form>
29										<p class="bg-danger text-white px-3 py-4">
30											An error occured. You cannot access this page. Please continue to <a class="text-white" on:click="{goLogin}">login page</a>.
31										</p>
32									</form>
33								</div>
34							</div>
35							<div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
36								<img src="/lofidrox/img/error_error.png" class="lfd-image-illu" alt="Error">
37							</div>
38						</div>
39					</div>
40				</div>
41			</div>
42		</div>
43	</section>
44</main>

Composant cmpLfdFileInbox.svelte

Ce composant affiche les fichiers reçus par l’utilisateur et permet leur téléchargement. La difficulté est de télécharger les fichiers un par un (la plupart des autres logiciels du même type imposent un téléchargement groupé).

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7	import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
  8	import GtgUtilsDates from '../../frameworks/gtg-svelte/gtg-utils-dates.js';
  9	import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 10	import LfdUrls from '../js/lfdUrlManager.js';
 11
 12	let hasFiles = false;
 13	let files = [ ];
 14	let selectedFiles = [ ];
 15	let hasSelectionDown = true;
 16	let hasSelectionDel = true;
 17
 18    function processNextFile(index) {
 19        let dataToSend;
 20        if (index < selectedFiles.length) {
 21            dataToSend = { id: selectedFiles[index] };
 22            GtgUtilsWeb.postJson(LfdUrls.crud_file_down, dataToSend, function(data) {
 23                if (!GtgUtilsTools.isNull(data)) {
 24					GtgUtilsWeb.download(data.data, data.filename, data.type);
 25                    processNextFile(++index);
 26                }
 27            }, function(error) {
 28                GtgUtilsWeb.route(LfdUrls.spa_error);
 29            } );
 30        } else {
 31            // erase selection
 32            selectedFiles = [ ];
 33            hasSelectionDown = true;
 34            // refresh list
 35            doRefresh();
 36        }
 37    };
 38
 39	function doDownload() {
 40		hasSelectionDown = ((null != selectedFiles) && (0 < selectedFiles.length));
 41		if (hasSelectionDown) {
 42			processNextFile(0);
 43		}
 44	};
 45
 46	function doRefresh() {
 47		fetchFiles();
 48	};
 49
 50	function doDelete() {
 51		let dataToSend = { files: null };
 52		hasSelectionDel = ((null != selectedFiles) && (0 < selectedFiles.length));
 53		if (hasSelectionDel) {
 54			dataToSend.files = selectedFiles;
 55			GtgUtilsWeb.deleteJson(LfdUrls.crud_file_del, dataToSend, function(data) {
 56                if (!GtgUtilsTools.isNull(data)) {
 57					fetchFiles();
 58                }
 59            }, function(error) {
 60                GtgUtilsWeb.route(LfdUrls.spa_error);
 61            } );
 62		}
 63	};
 64
 65    function fetchFiles() {
 66        files = [ ];
 67        GtgUtilsWeb.postJson(LfdUrls.crud_file_list, null, function(data) {
 68            if (!GtgUtilsTools.isNull(data)) {
 69                files = data.files;
 70                hasFiles = (!GtgUtilsTools.isNull(data.files) && (0 < data.files.length));
 71                clearSelection();
 72            }
 73        }, function(error) {
 74            GtgUtilsWeb.route(LfdUrls.spa_error);
 75        } );
 76    };
 77
 78	function clearSelection() {
 79		selectedFiles = [ ];
 80		hasSelectionDown = true;
 81		hasSelectionDel = true;
 82	};
 83
 84    fetchFiles();
 85</script>
 86
 87<main>
 88	<div class="row">
 89		<div class="col mx-1 my-2">
 90			<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
 91				<div class="container-fluid">
 92					<span class="navbar-brand text-warning lfd-title">Inbox</span>
 93					<div>
 94						<a class="btn btn-sm btn-outline-danger" on:click={doDelete}><i class="mdi mdi-delete-outline"></i></a>
 95						<span class="text-secondary">&nbsp;</span><a class="btn btn-sm btn-outline-info" on:click={doRefresh}>Refresh&nbsp;<i class="mdi mdi-refresh"></i></a>
 96						<span class="text-secondary">&nbsp;</span><a class="btn btn-sm btn-outline-primary" on:click={doDownload}>Download&nbsp;<i class="mdi mdi-download"></i></a>
 97					</div>
 98				</div>
 99			</nav>
100		</div>
101	</div>
102	<div class="row">
103		<div class="col mx-1 my-2">
104			<div class="lfd-files-list">
105				{#if !hasFiles }
106					<p class="bg-warning text-black px-1 py-1">You have no file to download</p>
107				{/if}
108				{#if !hasSelectionDown }
109					<p class="bg-danger text-white px-1 py-1">You must choose as least one file to download</p>
110				{/if}
111				{#if !hasSelectionDel }
112					<p class="bg-danger text-white px-1 py-1">You must choose as least one file to delete</p>
113				{/if}
114				<div class="lfd-card-list-content">
115					<div class="content">
116						<div class="row lfd-files-header">
117							<div class="col-sm-8 text-warning">Filename</div>
118							<div class="col-sm-4 text-warning">Sent by / on</div>
119						</div>
120						{#if files}
121							<div class="row lfd-files-list-data">
122								{#each files as file, idx}
123									<div class="col-sm-8 lfd-row-color-{idx % 2} text-white">
124										<input type="checkbox" name="user" value="{file.id}" bind:group={selectedFiles} />
125										{#if file.isNew}<i class="mdi mdi-alert-decagram text-danger"></i>{/if}
126										{file.filename}
127									</div>
128									<div class="col-sm-4 lfd-row-color-{idx % 2} text-white">{file.sentBy}, {file.sentOn}</div>
129								{/each}
130							</div>
131						{/if}
132					</div>
133				</div>
134			</div>
135		</div>
136	</div>
137</main>

Composant cmpLfdFileUpload.svelte

Ce composant permet d’envoyer un ou plusieurs fichiers à un ou plusieurs utilisateurs. Comme lors du téléchargement, l’envoi au serveur est réalisé fichier par fichier.

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7	import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
  8	import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
  9	import LfdUrls from '../js/lfdUrlManager.js';
 10
 11	let files = null;
 12	let filesDsp = null;
 13	let users = null;
 14	let selectedUsers = [ ];
 15	let hasUsers = true;
 16	let hasFiles = true;
 17
 18    function doProcess() {
 19        hasUsers = (0 !== selectedUsers.length);
 20		hasFiles = (GtgUtilsTools.isArray(filesDsp) && (0 !== filesDsp.length));
 21
 22        if (hasUsers && hasFiles) {
 23            processNextFile(0);
 24        }
 25    };
 26
 27	function handleFileUploadChange() {
 28		filesDsp = [];
 29		if (0 < files.length) {
 30			for (let pos = 0; pos < files.length; pos++) {
 31				filesDsp = [...filesDsp, { name: files[pos].name, uploaded: false, uploading: false } ];
 32			}
 33		}
 34	};
 35
 36    function processNextFile(index) {
 37        let fileRead;
 38
 39        if (index < files.length) {
 40            filesDsp[index] = { name: filesDsp[index].name, uploaded: false, uploading: true };
 41            fileRead = new FileReader();
 42            fileRead.addEventListener("load", function(evt) {
 43				let dataToSend = {
 44					filename: files[index].name,
 45					users: selectedUsers,
 46					data: fileRead.result
 47				};
 48                GtgUtilsWeb.postJson(LfdUrls.crud_file_send, dataToSend, function(data) {
 49	                if (!GtgUtilsTools.isNull(data) && data.written) {
 50	                    filesDsp[index] = { name: filesDsp[index].name, uploaded: true, uploading: false };
 51	                    processNextFile(++index);
 52	                }
 53	            }, function(error) {
 54	                GtgUtilsWeb.route(LfdUrls.spa_error);
 55	            } );
 56            }, false);
 57			fileRead.readAsDataURL(files[index]);
 58        }
 59    };
 60
 61    function fetchUsers() {
 62        GtgUtilsWeb.postJson(LfdUrls.crud_user_list, null, function(data) {
 63            if (!GtgUtilsTools.isNull(data)) {
 64                users = data.users;
 65            }
 66        }, function(error) {
 67            GtgUtilsWeb.route(LfdUrls.spa_error);
 68        } );
 69    };
 70
 71    function doClearFiles() {
 72        filesDsp = [ ];
 73        hasFiles = true;
 74    };
 75
 76	function doClearUsers() {
 77        selectedUsers = [ ];
 78        hasUsers = true;
 79    };
 80
 81    fetchUsers();
 82</script>
 83
 84<main>
 85	<div class="row">
 86		<div class="col mx-1 my-2">
 87			<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
 88				<div class="container-fluid">
 89					<span class="navbar-brand text-warning lfd-title">Upload</span>
 90				</div>
 91			</nav>
 92		</div>
 93	</div>
 94	<div class="row">
 95		<div class="col-lg-4">
 96			<div class="row">
 97				<div class="col mx-1 my-2">
 98					<nav class="navbar navbar-dark bg-dark lfd-files-header">
 99						<div class="container-fluid">
100							<span class="navbar-brand text-warning">Users available</span>
101							<div>
102								<div>
103									<a class="btn btn-sm btn-outline-secondary float-end" on:click={doClearUsers}>Clear</a>
104								</div>
105							</div>
106						</div>
107					</nav>
108				</div>
109			</div>
110			<div class="row">
111				<div class="col mx-1 my-2">
112					<div class="lfd-files-list">
113						{#if !hasUsers }
114							<p class="bg-danger text-white px-1 py-1">You must choose as least one user</p>
115						{/if}
116						<div class="lfd-files-list-content">
117							<div class="content">
118								{#if users}
119									{#each users as name}
120										<div class="is-size-7">
121											<input type="checkbox" name="user" value="{name}" bind:group={selectedUsers} />
122											<span class="text-white">{name}</span>
123										</div>
124									{/each}
125								{/if}
126							</div>
127						</div>
128					</div>
129				</div>
130			</div>
131		</div>
132		<div class="col-lg-8">
133			<div class="row">
134				<div class="col mx-1 my-2">
135					<nav class="navbar navbar-dark bg-dark lfd-files-header">
136						<div class="container-fluid">
137							<span class="navbar-brand text-warning">Files to send</span>
138							<div>
139								<div>
140									<a class="btn btn-sm btn-outline-secondary" on:click={doClearFiles}>Clear</a>
141									<span class="text-secondary">&nbsp;</span>
142									<a class="btn btn-sm btn-outline-primary float-end" on:click={doProcess}>Send&nbsp;<i class="mdi mdi-file-send"></i></a>
143								</div>
144							</div>
145						</div>
146					</nav>
147				</div>
148			</div>
149			<div class="row">
150				<div class="col mx-1 my-2">
151					<div class="lfd-files-list">
152						{#if !hasFiles }
153							<p class="bg-danger text-white px-1 py-1">You must choose as least one file</p>
154						{/if}
155						<div class="lfd-files-list-content">
156							<div class="content">
157								<input id="fileUpload" type="file" multiple bind:files on:change={handleFileUploadChange} />
158								{#if filesDsp}
159									{#each filesDsp as send}
160									<div class="is-size-7">
161										{#if !send.uploading && !send.uploaded}
162											<span class="icon text-danger"><i class="mdi mdi-timer-sand"></i></span>
163										{/if}
164										{#if send.uploading}
165										<span class="icon text-primary"><i class="mdi mdi-progress-clock"></i></span>
166										{/if}
167										{#if send.uploaded}
168										<span class="icon text-success"><i class="mdi mdi-check-circle-outline"></i></span>
169										{/if}
170										&nbsp;<span class="text-white">{send.name}</span>
171									</div>
172									{/each}
173								{/if}
174							</div>
175						</div>
176					</div>
177				</div>
178			</div>
179		</div>
180	</div>
181</main>

Composant cmpLfdHome.svelte

Ce composant affiche une page d’accueil proposant d’accéder aux fichiers reçus, à l’envoi de fichiers ainsi qu’à l’écran de déconnexion.

    
 1<style>
 2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
 3    @import "../css/lofidrox.css";
 4</style>
 5
 6<script>
 7    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 8    import LfdUrls from '../js/lfdUrlManager.js';
 9
10    function goInbox() {
11        GtgUtilsWeb.route(LfdUrls.spa_file_inbox);
12    };
13
14    function goUpload() {
15        GtgUtilsWeb.route(LfdUrls.spa_file_upload);
16    };
17
18    function goLogout() {
19        GtgUtilsWeb.route(LfdUrls.spa_user_logout);
20    };
21</script>
22
23<main>
24    <div class="row">
25        <div class="col-lg-12 text-center">&nbsp;</div>
26    </div>
27    <div class="row">
28        <div class="col-lg-12 text-center">
29            <h3 class="text-white">
30                Welcome on&nbsp;<span class="lfd-title text-warning">LoFiDroX</span>
31            </h3>
32        </div>
33    </div>
34    <div class="row">
35        <div class="col-lg-12 text-center">&nbsp;</div>
36    </div>
37    <div class="row gx-5 text-center">
38        <div class="col-lg-4 d-flex align-items-center justify-content-center">
39            <div class="card" style="width: 18rem;">
40                <img src="/lofidrox/img/home_inbox.png" class="card-img-top" alt="Inbox">
41                <div class="card-body">
42                    <h5 class="card-title">Inbox</h5>
43                    <p class="card-text">To consult files you received</p>
44                    <a class="btn btn-sm btn-outline-primary" on:click={goInbox} >Inbox</a>
45                </div>
46            </div>
47        </div>
48        <div class="col-lg-4 d-flex align-items-center justify-content-center">
49            <div class="card" style="width: 18rem;">
50                <img src="/lofidrox/img/home_upload.png" class="card-img-top" alt="Upload">
51                <div class="card-body">
52                    <h5 class="card-title">Upload</h5>
53                    <p class="card-text">To send files to other users</p>
54                    <a class="btn btn-sm btn-outline-primary" on:click={goUpload}>Upload</a>
55                </div>
56            </div>
57        </div>
58        <div class="col-lg-4 d-flex align-items-center justify-content-center">
59            <div class="card" style="width: 18rem;">
60                <img src="/lofidrox/img/home_logout.png" class="card-img-top" alt="Logout">
61                <div class="card-body">
62                    <h5 class="card-title">Logout</h5>
63                    <p class="card-text">To quit&nbsp;<span class="lfd-app-name">LoFiDroX</span></p>
64                    <a class="text-danger btn btn-sm btn-outline-danger" on:click={goLogout} >Logout</a>
65                </div>
66            </div>
67        </div>
68    </div>
69</main>

Composant cmpLfdTitle.svelte

Il s’agit du bandeau de titre avec les boutons permettant d’accéder aux fonctions principales (fichiers reçus, envoi de fichiers, déconnexion).

    
 1<style>
 2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
 3    @import "../css/lofidrox.css";
 4</style>
 5
 6<script>
 7    import { lfdStore } from '../js/lfdStore.js';
 8    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 9    import LfdUrls from '../js/lfdUrlManager.js';
10
11    let loggedIn = false;
12
13    $: loggedIn = $lfdStore.loggedIn;
14
15    function goHome() {
16        GtgUtilsWeb.route(LfdUrls.spa_home);
17    };
18
19    function goInbox() {
20        GtgUtilsWeb.route(LfdUrls.spa_file_inbox);
21    };
22
23    function goUpload() {
24        GtgUtilsWeb.route(LfdUrls.spa_file_upload);
25    };
26
27    function goLogout() {
28        GtgUtilsWeb.route(LfdUrls.spa_user_logout);
29    };
30</script>
31
32<nav class="navbar navbar-expand-lg navbar-dark bg-dark lfd-nav-bar">
33    <div class="container-fluid">
34        {#if loggedIn}
35            <a on:click={goHome} ><img src="/lofidrox/img/lofidrox_small.png"/></a>
36        {:else}
37            <img src="/lofidrox/img/lofidrox_small.png"/>
38        {/if}
39        <span class="navbar-brand lfd-title">LoFiDroX</span>
40        <div>
41            <div>
42                {#if loggedIn}
43                <a class="text-secondary btn btn-sm btn-outline-secondary" on:click={goHome} >Home</a>
44                <span class="text-secondary">&nbsp;</span>
45                <a class="text-white btn btn-sm btn-outline-light" on:click={goInbox} >Inbox</a>
46                <span class="text-secondary">&nbsp;</span>
47                <a class="text-white btn btn-sm btn-outline-light" on:click={goUpload} >Upload</a>
48                <span class="text-secondary">&nbsp;</span>
49                <a class="text-danger btn btn-sm btn-outline-danger" on:click={goLogout} >Logout</a>
50                {/if}
51            </div>
52        </div>
53    </div>
54</nav>

Composant cmpLfdUserLogin.svelte

Ce composant est l’écran de connexion. Il intègre le contrôle des champs via le framework Yup.

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7    import { navigate } from "svelte-routing";
  8    import { onMount } from 'svelte';
  9    import * as yup from 'yup';
 10    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 11    import GtgUtilsPassword from '../../frameworks/gtg-svelte/gtg-utils-password.js';
 12    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 13    import LfdUrls from '../js/lfdUrlManager.js';
 14    import LfdEvents from '../js/lfdEventsManager.js';
 15    import LfdLocalStore from '../js/lfdLocalStore.js';
 16    import { lfdStore } from '../js/lfdStore.js';
 17
 18    let fields = { fieldUsr: null, fieldPwd: null };
 19    let values = { username: null, password: null };
 20    let validation = { login: true };
 21    let errorMessages = { username: null, pwd: null };
 22
 23    let schemaValidation = yup.object().shape( {
 24        username: yup.string().nullable().required('You must enter a username').min(4, 'Username must contains at least ${min} letters'),
 25        pwd: yup.string().nullable().required('You must enter a password').
 26            min(10, 'Password must contains at least ${min} caracters').
 27            max(256, 'Passqord must contains up to ${max} caracters long').
 28            test('pwd-strength', 'Password must contains uppercase letter, lowercase letter, digit, special caracter',
 29            function(value) {
 30                return GtgUtilsPassword.checkStrongPwd(value);
 31            } )
 32    } );
 33
 34    async function login() {
 35        try {
 36            validation.login = true;
 37            errorMessages = { username: null, pwd: null };
 38            await schemaValidation.validate( { 'username': values.username, 'pwd': values.password }, { abortEarly: false } );
 39            GtgUtilsTools.setLocalValue(LfdLocalStore.field_username, values.username);
 40            GtgUtilsWeb.postJson(LfdUrls.crud_user_login, { 'username': values.username, 'pwd': values.password }, function(data) {
 41                if (!GtgUtilsTools.isNull(data) && !GtgUtilsTools.isNull(data.logged) && data.logged) {
 42                    $lfdStore.loggedIn = true;
 43                    GtgUtilsWeb.route(LfdUrls.spa_home);
 44                }
 45            }, function(error) {
 46                if (!GtgUtilsTools.isNull(error) && (GtgUtilsWeb.HTTP_CODES.Unauthorized === error)) {
 47                    validation.login = false;
 48                } else {
 49                    GtgUtilsWeb.route(LfdUrls.spa_error);
 50                }
 51            } );
 52        } catch (errors) {
 53            errorMessages = GtgUtilsTools.turnYupErrorsToArray(errors);
 54        }
 55    };
 56
 57    function goRegister() {
 58        GtgUtilsWeb.route(LfdUrls.spa_user_register);
 59    };
 60
 61    values.username = window.localStorage.getItem(LfdLocalStore.field_username);
 62
 63    onMount(() => {
 64        if (!GtgUtilsTools.isNull(values.username)) {
 65            fields.fieldPwd.focus();
 66        } else {
 67            fields.fieldUsr.focus();
 68        }
 69    } );
 70</script>
 71
 72<main class="container">
 73    <section class="h-100 lfd-gradient-form">
 74        <div class="container py-2 h-100">
 75            <div class="row d-flex justify-content-center align-items-center h-100">
 76                <div class="col-xl-10">
 77                    <div class="card rounded-3 text-black">
 78                        <div class="row g-0">
 79                            <div class="col-md-6">
 80                                <div class="card-body p-md-5 mx-md-4">
 81                                    <div class="text-center">
 82                                        <img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
 83                                        <h4 class="mt-1 mb-5 pb-1"></h4>
 84                                    </div>
 85                                    <form on:submit|preventDefault|stopPropagation={(e)=>{login();}}>
 86                                        <p>Please login to your account</p>
 87                                        <div class="form-outline mb-4">
 88                                            <input class="input form-control form-control-sm" tabindex="1" type="text" name="fieldUsr" placeholder="Enter your username" bind:value={values.username} bind:this={fields.fieldUsr}>
 89                                            {#if errorMessages.username }
 90                                            <p class="bg-danger text-white px-1 py-1">{errorMessages.username}</p>
 91                                            {/if}
 92                                        </div>
 93                                        <div class="form-outline mb-4">
 94                                            <input class="input form-control form-control-sm" tabindex="2" type="password" name="fieldPwd" placeholder="Enter your password" bind:value={values.password} bind:this={fields.fieldPwd}>
 95                                            {#if errorMessages.pwd }
 96                                            <p class="bg-danger text-white px-1 py-1">{errorMessages.pwd}</p>
 97                                            {/if}
 98                                        </div>
 99                                        {#if !validation.login }
100                                        <div class="form-outline mb-4">
101                                            <p class="bg-danger text-white px-1 py-1">
102                                                You cannot login. Please check your login and password.
103                                            </p>
104                                        </div>
105                                        {/if}
106                                        <div class="text-center pt-1 mb-5 pb-1">
107                                            <button class="btn btn-primary btn-block fa-lg lfd-gradient-colors mb-3" tabindex="3" type="submit">Log in&nbsp;<i class="mdi mdi-login" aria-hidden="true"></i></button>
108                                        </div>
109                                        <div class="d-flex align-items-center justify-content-center pb-4">
110                                            <p class="mb-0 me-2">Don't have an account?</p>
111                                            <a type="button" class="btn btn-outline-primary" on:click="{goRegister}" tabindex="4">Create&nbsp;<i class="mdi mdi-account-plus"></i></a>
112                                        </div>
113                                    </form>
114                                </div>
115                            </div>
116                            <div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
117                                {#if validation.login }
118                                    <div class="text-white px-3 py-4 p-md-5 mx-md-4">
119                                        <p class="small mb-0"><span class="lfd-app-name">LoFiDroX</span>&nbsp;let you share files over a Local Area Network.&nbsp;
120                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;don't use cloud to synchronize content: files are stored on you local&nbsp;
121                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;instance.
122                                        </p>
123                                    </div>
124                                {:else}
125                                    <img src="/lofidrox/img/user_login_error.png" class="lfd-image-illu" alt="Error">
126                                {/if}
127                            </div>
128                        </div>
129                    </div>
130                </div>
131            </div>
132        </div>
133    </section>
134</main>

Composant cmpLfdUserLogout.svelte

Ce composant affiche l’écran de déconnexion.

    
 1<style>
 2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
 3    @import "../css/lofidrox.css";
 4</style>
 5
 6<script>
 7    import { navigate } from "svelte-routing";
 8    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 9    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
10    import LfdUrls from '../js/lfdUrlManager.js';
11    import LfdLocalStore from '../js/lfdLocalStore.js';
12    import { lfdStore } from '../js/lfdStore.js';
13
14    function logout() {
15        GtgUtilsWeb.deleteJson(LfdUrls.crud_user_logout, { 'username': GtgUtilsTools.getLocalValue(LfdLocalStore.field_username) }, function(data) {
16            if (!GtgUtilsTools.isNull(data) && !GtgUtilsTools.isNull(data.exited) && data.exited) {
17                $lfdStore.loggedIn = false;
18                GtgUtilsWeb.route(LfdUrls.spa_user_login);
19            }
20        }, function(error) {
21            GtgUtilsWeb.route(LfdUrls.spa_error);
22        } );
23    };
24
25    function goBack() {
26        GtgUtilsWeb.route(LfdUrls.spa_home);
27    };
28</script>
29
30<main class="container">
31    <section class="h-100 lfd-gradient-form">
32        <div class="container py-2 h-100">
33            <div class="row d-flex justify-content-center align-items-center h-100">
34                <div class="col-xl-10">
35                    <div class="card rounded-3 text-black">
36                        <div class="row g-0">
37                            <div class="col-md-6">
38                                <div class="card-body p-md-5 mx-md-4">
39                                    <div class="text-center">
40                                        <img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
41                                        <h4 class="mt-1 mb-5 pb-1"></h4>
42                                    </div>
43                                    <form>
44                                        <p class="bg-danger text-white px-3 py-4">Are you sure you want to exit <span class="lfd-app-name">LoFiDroX</span>?</p>
45                                        <div class="text-center pt-1 mb-5 pb-1">
46                                            <a class="btn btn-danger btn-block fa-lg mb-3" type="button" on:click="{logout}">Logout&nbsp;<i class="mdi mdi-logout"></i></a>
47                                            <a class="btn btn-outline-dark fa-lg mb-3" on:click="{goBack}">Cancel&nbsp;<i class="mdi mdi-close-circle-outline"></i></a>
48                                        </div>
49                                    </form>
50                                </div>
51                            </div>
52                            <div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
53                                <img src="/lofidrox/img/user_logout_logout.png" class="lfd-image-illu" alt="Logout">
54                            </div>
55                        </div>
56                    </div>
57                </div>
58            </div>
59        </div>
60    </section>
61</main>

Composant cmpLfdUserRegister.svelte

Ce composant est l’écran de création de compte. Il est très similaire à l’écran de connexion.

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7    import { onMount } from 'svelte';
  8    import * as yup from 'yup';
  9    import LfdUrls from '../js/lfdUrlManager.js';
 10    import LfdStore from '../js/lfdLocalStore.js';
 11    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 12    import GtgUtilsPassword from '../../frameworks/gtg-svelte/gtg-utils-password.js';
 13    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 14
 15    let fields = { fUsername: null };
 16    let values = { username: null, password: null, passwordConf: null };
 17    let validation = { registered: true };
 18    let errorMessages = { username: null, pwd: null, pwdCnf: null };
 19
 20    let schemaValidation = yup.object().shape( {
 21        username: yup.string().nullable().required('You must enter a username').min(4, 'Username must contains at least ${min} letters'),
 22        pwd: yup.string().nullable().required('You must enter a password').
 23            min(10, 'Password must contains at least ${min} caracters').
 24            max(256, 'Passqord must contains up to ${max} caracters long').
 25            test('pwd-strength', 'Password must contains uppercase letter, lowercase letter, digit, special caracter',
 26            function(value) {
 27                return GtgUtilsPassword.checkStrongPwd(value);
 28            } ),
 29        pwdCnf: yup.string().nullable().required('You must confirm password').
 30            min(10, 'Password must contains at least ${min} caracters').
 31            max(256, 'Passqord must contains up to ${max} caracters long').
 32            test('pwd-egality', 'Passwords are not identical',
 33            function(value) {
 34                return GtgUtilsPassword.checkEquality(values.password, value);
 35            } )
 36    } );
 37
 38    function goLogin() {
 39        GtgUtilsWeb.route(LfdUrls.spa_user_login);
 40    };
 41
 42    async function register() {
 43        try {
 44            validation.registered = true;
 45            errorMessages = { username: null, pwd: null, pwdCnf: null };
 46            await schemaValidation.validate( { 'username': values.username, 'pwd': values.password, 'pwdCnf': values.passwordConf }, { abortEarly: false } );
 47            GtgUtilsWeb.postJson(LfdUrls.crud_user_register, { username: values.username, pwd: values.password }, function(data) {
 48                if ((null != data) && data.registered) {
 49                    window.localStorage.setItem(LfdStore.field_username, data.username);
 50                    GtgUtilsWeb.route(LfdUrls.spa_user_login);
 51                }
 52            }, function(error) {
 53                if (!GtgUtilsTools.isNull(error) && (GtgUtilsWeb.HTTP_CODES.NotAcceptable === error)) {
 54                    validation.registered = false;
 55                } else {
 56                    GtgUtilsWeb.route(LfdUrls.spa_error);
 57                }
 58            } );
 59        } catch (errors) {
 60            errorMessages = GtgUtilsTools.turnYupErrorsToArray(errors);
 61        }
 62    };
 63
 64    onMount(() => {
 65        fields.fUsername.focus();
 66    } );
 67</script>
 68
 69<main class="container">
 70    <section class="h-100 lfd-gradient-form">
 71        <div class="container py-2 h-100">
 72            <div class="row d-flex justify-content-center align-items-center h-100">
 73                <div class="col-xl-10">
 74                    <div class="card rounded-3 text-black">
 75                        <div class="row g-0">
 76                            <div class="col-md-6">
 77                                <div class="card-body p-md-5 mx-md-4">
 78                                    <div class="text-center">
 79                                        <img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
 80                                        <h4 class="mt-1 mb-5 pb-1"></h4>
 81                                    </div>
 82                                    <form on:submit|preventDefault|stopPropagation={(e)=>{register();}}>
 83                                        <p>Please create your account</p>
 84                                        <div class="form-outline mb-4">
 85                                            <input class="input form-control form-control-sm" tabindex="1" type="text" placeholder="Enter your name" id="fUsername" bind:value={values.username} bind:this={fields.fUsername}>
 86                                            {#if errorMessages.username }
 87                                                <p class="bg-danger text-white px-1 py-1">{errorMessages.username}</p>
 88                                            {/if}
 89                                        </div>
 90                                        <div class="form-outline mb-4">
 91                                            <input class="input form-control form-control-sm" tabindex="2" type="password" placeholder="Enter your password" id="fPassword" bind:value="{values.password}">
 92                                            {#if errorMessages.pwd}
 93                                                <p class="bg-danger text-white px-1 py-1">{errorMessages.pwd}</p>
 94                                            {/if}
 95                                        </div>
 96                                        <div class="form-outline mb-4">
 97                                            <input class="input form-control form-control-sm" tabindex="3" type="password" placeholder="Confirm your password" id="fPasswordCnf" bind:value="{values.passwordConf}">
 98                                            {#if errorMessages.pwdCnf}
 99                                                <p class="bg-danger text-white px-1 py-1">{errorMessages.pwdCnf}</p>
100                                            {/if}
101                                        </div>
102                                        {#if !validation.registered }
103                                            <div class="form-outline mb-4">
104                                                <p class="bg-danger text-white px-1 py-1">
105                                                    You cannot register this account. Please use another username.
106                                                </p>
107                                            </div>
108                                        {/if}
109                                        <div class="text-center pt-1 mb-5 pb-1">
110                                            <button class="btn btn-primary btn-block fa-lg mb-3" tabindex="4" type="submit">Register&nbsp;<i class="mdi mdi-account-plus"></i></button>
111                                            <a class="btn btn-outline-dark fa-lg mb-3" tabindex="5" type="button" on:click="{goLogin}">Cancel&nbsp;<i class="mdi mdi-close"></i></a>
112                                        </div>
113                                    </form>
114                                </div>
115                            </div>
116                            <div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
117                                {#if validation.registered }
118                                    <img src="/lofidrox/img/user_register_register.png" class="lfd-image-illu" alt="Register">
119                                {:else}
120                                    <img src="/lofidrox/img/user_register_error.png" class="lfd-image-illu" alt="Error">
121                                {/if}
122                            </div>
123                        </div>
124                    </div>
125                </div>
126            </div>
127        </div>
128    </section>
129</main>

Composant cmpLoFiDroX.svelte

Enfin le composant principal: son rôle est d’afficher le composant demandé (on parle alors de routage).

    
 1<style>
 2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
 3    @import "../css/lofidrox.css";
 4</style>
 5
 6<script>
 7    import { Router, Link, Route } from "svelte-routing";
 8    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 9    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
10    import LfdUrls from '../js/lfdUrlManager.js';
11    import LfdLocalStore from '../js/lfdLocalStore.js';
12    import { lfdStore } from '../js/lfdStore.js';
13    import CmpLfdTitle from './cmpLfdTitle.svelte';
14    import CmpLfdHome from './cmpLfdHome.svelte';
15    import CmpLfdUserLogin from './cmpLfdUserLogin.svelte';
16    import CmpLfdUserRegister from './cmpLfdUserRegister.svelte';
17    import CmpLfdUserLogout from './cmpLfdUserLogout.svelte';
18    import CmpLfdFileUpload from './cmpLfdFileUpload.svelte';
19    import CmpLfdFileInbox from './cmpLfdFileInbox.svelte';
20    import CmpLfdError from './cmpLfdError.svelte';
21
22    let container = { };
23    // fetch query string parameters
24    container.urlParams = new URLSearchParams(window.location.search);
25    // fetch redirect page
26    container.page = container.urlParams.get('page');
27    // check if user session is still valid
28    container.username = window.localStorage.getItem(LfdLocalStore.field_username);
29    // define default route
30    container.urlSpa = LfdUrls.spa_user_login;
31
32    // skip user session check if no username found
33    if (!GtgUtilsTools.isNullOrEmpty(container.username)) {
34        // send check request
35        GtgUtilsWeb.postJson(LfdUrls.crud_user_check, { username: container.username }, function(data) {
36            if (!GtgUtilsTools.isNull(data) && data.session) {
37                // user's session is still valid, goto to homepage
38                $lfdStore.loggedIn = true;
39                GtgUtilsWeb.route(LfdUrls.spa_home);
40            } else {
41                // session is now invalid. If requested page is error, show it otherwise show login page
42                GtgUtilsWeb.route((LfdUrls.spa_error !== container.page? LfdUrls.spa_user_login: container.page));
43            }
44        }, function(error) {
45            // an error occured, goto to login page
46            if (!GtgUtilsTools.isNull(error) && (GtgUtilsWeb.HTTP_CODES.Unauthorized === error)) {
47                GtgUtilsWeb.route(LfdUrls.spa_user_login);
48            } else {
49                GtgUtilsWeb.route(LfdUrls.spa_error);
50            }
51        } );
52    };
53
54    // define default route
55    //export let urlSpa = LfdUrls.spa_user_login;
56</script>
57
58<main class="hero is-fullheight lfd-scroller-hori-no lfd-scroller-vert-no">
59    <div class="hero-head">
60        <CmpLfdTitle />
61    </div>
62    <div class="hero-body">
63        <div class="container lfd-container-main">
64            <Router url="{container.urlSpa}">
65                <Route path="{LfdUrls.spa_error}"><CmpLfdError /></Route>
66                <Route path="{LfdUrls.spa_home}"><CmpLfdHome /></Route>
67                <Route path="{LfdUrls.spa_user_login}"><CmpLfdUserLogin /></Route>
68                <Route path="{LfdUrls.spa_user_register}"><CmpLfdUserRegister /></Route>
69                <Route path="{LfdUrls.spa_user_logout}"><CmpLfdUserLogout /></Route>
70                <Route path="{LfdUrls.spa_file_upload}"><CmpLfdFileUpload /></Route>
71                <Route path="{LfdUrls.spa_file_inbox}"><CmpLfdFileInbox /></Route>
72            </Router>
73        </div>
74    </div>
75</main>

Conclusion

Svelte propose une structure simple et efficace pour réaliser des applications de type “Single Page” (SPA).

C’était le dernier article expliquant les rouages de l’application LoFiDroX. Si tu as des questions, poses les moi par mail.

Code source

Comme annoncé dans l’article Fuyez GitHub, le code n’est plus disponible sur GitHub. L’intégralité du code est disponible sur mon CodeBerg: https://codeberg.org/GoboTheGeek/LoFiDroX