✨ Story 5.3: persistance localStorage
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
/**
|
||||
* Validation du formulaire de contact
|
||||
* JavaScript vanilla - pas de dépendances
|
||||
* JavaScript vanilla - pas de dépendances
|
||||
*/
|
||||
|
||||
class FormValidator {
|
||||
@@ -21,13 +21,13 @@ class FormValidator {
|
||||
required: true,
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
message: 'Veuillez entrer votre nom (2 caractères minimum)'
|
||||
message: 'Veuillez entrer votre nom (2 caractères minimum)'
|
||||
},
|
||||
prenom: {
|
||||
required: true,
|
||||
minLength: 2,
|
||||
maxLength: 100,
|
||||
message: 'Veuillez entrer votre prénom (2 caractères minimum)'
|
||||
message: 'Veuillez entrer votre prénom (2 caractères minimum)'
|
||||
},
|
||||
email: {
|
||||
required: true,
|
||||
@@ -36,19 +36,19 @@ class FormValidator {
|
||||
},
|
||||
categorie: {
|
||||
required: true,
|
||||
message: 'Veuillez sélectionner une catégorie'
|
||||
message: 'Veuillez sélectionner une catégorie'
|
||||
},
|
||||
objet: {
|
||||
required: true,
|
||||
minLength: 5,
|
||||
maxLength: 200,
|
||||
message: 'Veuillez entrer un objet (5 caractères minimum)'
|
||||
message: 'Veuillez entrer un objet (5 caractères minimum)'
|
||||
},
|
||||
message: {
|
||||
required: true,
|
||||
minLength: 20,
|
||||
maxLength: 5000,
|
||||
message: 'Veuillez entrer votre message (20 caractères minimum)'
|
||||
message: 'Veuillez entrer votre message (20 caractères minimum)'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -98,7 +98,7 @@ class FormValidator {
|
||||
|
||||
if (isValid && rule.maxLength && value.length > rule.maxLength) {
|
||||
isValid = false;
|
||||
errorMessage = `Maximum ${rule.maxLength} caractères`;
|
||||
errorMessage = `Maximum ${rule.maxLength} caractères`;
|
||||
}
|
||||
|
||||
if (isValid && rule.email && value) {
|
||||
@@ -212,6 +212,89 @@ class FormValidator {
|
||||
}
|
||||
}
|
||||
|
||||
class ContactFormPersistence {
|
||||
constructor(formId) {
|
||||
this.form = document.getElementById(formId);
|
||||
if (!this.form) return;
|
||||
|
||||
this.debounceTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadSavedData();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.form.addEventListener('input', () => {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(() => this.saveData(), 500);
|
||||
});
|
||||
|
||||
const clearBtn = document.getElementById('clear-form-btn');
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => this.clearForm());
|
||||
}
|
||||
|
||||
this.form.addEventListener('formSuccess', () => {
|
||||
AppState.clearFormData();
|
||||
});
|
||||
}
|
||||
|
||||
saveData() {
|
||||
const formData = new FormData(this.form);
|
||||
const data = {};
|
||||
|
||||
formData.forEach((value, key) => {
|
||||
data[key] = value;
|
||||
});
|
||||
|
||||
AppState.saveFormData(data);
|
||||
}
|
||||
|
||||
loadSavedData() {
|
||||
const savedData = AppState.getFormData();
|
||||
if (!savedData) return;
|
||||
|
||||
Object.keys(savedData).forEach((key) => {
|
||||
const field = this.form.querySelector(`[name="${key}"]`);
|
||||
if (field && savedData[key]) {
|
||||
field.value = savedData[key];
|
||||
}
|
||||
});
|
||||
|
||||
const messageField = this.form.querySelector('[name="message"]');
|
||||
const countEl = document.getElementById('message-count');
|
||||
if (messageField && countEl) {
|
||||
countEl.textContent = messageField.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
clearForm() {
|
||||
if (!confirm('Êtes-vous sûr de vouloir effacer le formulaire ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppState.clearFormData();
|
||||
this.form.reset();
|
||||
|
||||
const countEl = document.getElementById('message-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = '0';
|
||||
}
|
||||
|
||||
this.form.querySelectorAll('.input-error').forEach((el) => {
|
||||
el.classList.remove('input-error');
|
||||
});
|
||||
this.form.querySelectorAll('[data-error]').forEach((el) => {
|
||||
el.classList.add('hidden');
|
||||
el.textContent = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.contactFormValidator = new FormValidator('contact-form');
|
||||
window.contactFormPersistence = new ContactFormPersistence('contact-form');
|
||||
});
|
||||
|
||||
57
assets/js/state.js
Normal file
57
assets/js/state.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Gestionnaire d'état pour le localStorage
|
||||
*/
|
||||
const AppState = {
|
||||
STORAGE_KEY: 'portfolio_contact_form',
|
||||
EXCLUDED_FIELDS: ['csrf_token', 'password', 'recaptcha_token'],
|
||||
|
||||
isStorageAvailable() {
|
||||
try {
|
||||
const test = '__storage_test__';
|
||||
localStorage.setItem(test, test);
|
||||
localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
saveFormData(data) {
|
||||
if (!this.isStorageAvailable()) return;
|
||||
|
||||
try {
|
||||
const filteredData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (!this.EXCLUDED_FIELDS.includes(key)) {
|
||||
filteredData[key] = data[key];
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(filteredData));
|
||||
} catch (e) {
|
||||
console.warn('Impossible de sauvegarder dans localStorage:', e);
|
||||
}
|
||||
},
|
||||
|
||||
getFormData() {
|
||||
if (!this.isStorageAvailable()) return null;
|
||||
|
||||
try {
|
||||
const data = localStorage.getItem(this.STORAGE_KEY);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (e) {
|
||||
console.warn('Impossible de charger depuis localStorage:', e);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
clearFormData() {
|
||||
if (!this.isStorageAvailable()) return;
|
||||
|
||||
try {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
} catch (e) {
|
||||
// Silencieux
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user