diff --git a/.eslintrc.js b/.eslintrc.js index df66db4..6a307b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { "no-console": process.env.NODE_ENV === "production" ? "error" : "off", "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", "comma-dangle": [2, "always-multiline"], + "no-var": 2, }, overrides: [ { diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c3fe97..89d1240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,5 @@ # Changelog -## [1.0.13] - 2020-04-06 +## [1.0.0] - 2020-04-15 ### Added - first public release -## [1.1.0] - 2020-04-08 -### Added -- (#21, #23, #25, #26, #27, #28, #29) :wheelchair: improve A11Y support -- adds github links in footer -- (#19) clear localstorage -- prevent sending personal data -- 💚 Publish html as workflow asset and code in public repository -- Add sitemap and tell robots.txt to scan the sitemap -- Add Changelog -### Changes -- (#18) 📝 Update header for reasons section -- (#3) ✏ Replace codiv by covid in package.json, README -- changes start target -- (#1) 🐛 Autocomplete birthday field only on keyup, not when deleting -- (#1) 🎹 Improve regex control pattern for birhtday -- :recycle: Move check update code to dedicated file -- :recycle: Move dom utils in dedicated file -- :sparkles: Notify user about newer version -- :sparkles: Add cache with service workers -- :recycle: Cleanup directory structure -- :heavy_plus_sign: Avoid using env specific syntax in npm scripts -- changes PWA display -## [1.1.1] - 2020-04-09 -### Fixed -- :wrench: Fix parcel-plugin-sw-cache configuration -### Added -- :pencil: add CONTRIBUTORS -### Changes -- :pencil: change LICENCE ( #16 ) -## [1.1.2] - 2020-04-10 -### Added -- ♿ Increase contrast ratio hover link color (#29) -- ♿ Add landmarks roles to header, main and footer sections (#43) -- ♿ Add title to open stores link and add more explicit text (#44, #45) -- ♿ Expose aria-invalid field assistance technologies (#24) -- ✹ Add timestamp to generated pdf (#9) -- 💄 Don't use autocompletion for release date and time (#31) -- 💄 Add underline on hover links -### Fixes -- Fix Typo site.webmanifest (#52) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index a02fe4f..7c4c2f2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -1,20 +1,13 @@ -Ce gĂ©nĂ©rateur d'attestation de dĂ©placement dĂ©rogatoire a Ă©tĂ© mis en place dans le cadre du confinement liĂ© Ă  la pandĂ©mie du virus COVID-19 de 2020. -Ce service repose sur l'utilisation initale du projet covid-19-certificate de Johann Pardanaud . +Ce gĂ©nĂ©rateur d'attestation de dĂ©placement international dĂ©rogatoire a Ă©tĂ© mis en place dans le cadre du confinement liĂ© Ă  la pandĂ©mie du virus COVID-19 de 2020. +Ce service repose sur l'utilisation initale du projet covid-19-certificate de Johann Pardanaud et de l'attestation de dĂ©placement nationale dĂ©rogatoire du ministĂšre de l'intĂ©rieur. Il a Ă©tĂ© enrichi par l'incubateur du ministĂšre de l'intĂ©rieur : le LAB-MI . La liste qui suit mentionne les diffĂ©rents participants ayant contribuĂ© Ă  rendre ce service utile Ă  la fois Ă  la population et aux forces de l'ordre : - Johann Pardanaud (https://github.com/nesk) + +La liste qui suit mentionne les diffĂ©rents participants ayant contribuĂ© Ă  rendre ce service utile Ă  la fois Ă  la population et aux forces de l'ordre : + Philippe Bron (https://github.com/PhilippeBron) Cristian (https://github.com/cristianpb) - Stanislas OrmiĂšres (https://github.com/laruiss) - Caroline Robillard (https://github.com/Carolinedanslesnuages) - Joel Pagniez (https://github.com/JoelPagniez) - Sophie GUERLAIS - Philippe (https://github.com/pli01) - Matthieu Bacconnier (https://github.com/Neamar) - Hugo Cartigny (https://github.com/BlueskyFR) - SĂ©bastien TouzĂ© (https://github.com/SebastienTouze) - John Livingston (https://github.com/JohnXLivingston) - David Libeau (https://github.com/DavidLibeau) - Arnaud Delafosse (https://github.com/ArnaudDelafosse) + Victor JournĂ© (https://github.com/victorjourne) + Philippe (https://github.com/pli01) \ No newline at end of file diff --git a/README.md b/README.md index 31d86de..5ef6e90 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GĂ©nĂ©rateur de certificat de dĂ©placement +# GĂ©nĂ©rateur: ATTESTATION DE DÉPLACEMENT INTERNATIONAL DÉROGATOIRE VERS LA FRANCE MÉTROPOLITAINE ## DĂ©velopper diff --git a/doc-tools/conception.md b/doc-tools/conception.md new file mode 100644 index 0000000..cb142a6 --- /dev/null +++ b/doc-tools/conception.md @@ -0,0 +1,59 @@ +# Attestation de dĂ©placement international dĂ©rogatoire vers la France mĂ©tropolitaine + +## Fichiers pdf des attestations papier : + +- version française : 07-04-20-Attestation-etranger-metropole-FR.pdf +- version anglaise : Attestation_deplacement_International_EN-1.pdf + +## Analyse de la structure du document + +### Champs ajoutĂ©s : + +- nationalitĂ© +- typologie de nationalitĂ©s : pays tiers ; eu ou assimilĂ©s ; française + +### Champs supprimĂ©s : + +- lieu de naissance +- date de sortie +- heure de sortie + +### Remarques : + +- Les motifs sont identiques pour les types tiers et eu. +- Pas de motif pour les français. +- Un seul motif possible dĂ©fini en fonction du type de nationalitĂ© + +## Structure des champs du QR Code + +- **Cree le:** creationDate a creationHour;\n +- **Nom:** `firstname`;\n +- **Prenom:** `lastname`;\n +- **Naissance:** `birthday` (`nationality`);\n +- **Adresse:** `address` `zipcode` `town` `country`;\n +- **Sortie:** N/A;\n +- **Motifs:** `national`-`reason` (`reaseon` Ă  vide pour les français) + +### Correspondances formulaire/QR Code + +Afin de pouvoir ĂȘtre sotckĂ©es dans le QR Code, Ă  chaque choix du formulaire est associĂ© un alias. + +#### types de nationalitĂ©s + +| libellĂ© formulaire | alias | +|:-----|:-----| +| Ressortissants de pays tiers | tiers | +| Ressortissants de l’Union europĂ©enne et assimilĂ©s | eu | +| Ressortissants de nationalitĂ© française | fr | + +#### types de motifs + +| libellĂ© formulaire | alias | +|:-----|:-----| +| Personnes ayant leur rĂ©sidence principale en France | residence | +| Personnes transitant par la France pour rejoindre leur rĂ©sidence | transit | +| Professionnels de santĂ© aux fins de lutter contre le Covid-19 | prof._sante | +| Transporteurs de marchandises | marchandises | +| Equipages et personnels exploitant des vols | equipage | +| Personnels des missions diplomatiques et consulaires | diplomatique | +| Travailleurs frontaliers | frontalier | \ No newline at end of file diff --git a/doc-tools/grid.html b/doc-tools/grid.html new file mode 100644 index 0000000..1209dce --- /dev/null +++ b/doc-tools/grid.html @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index fd6b9e1..3e5e358 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "deplacement-covid-19", + "name": "deplacement-vers-france-covid-19", "version": "0.0.1", "lockfileVersion": 1, "requires": true, diff --git a/package.json b/package.json index caae57e..00c3e9e 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,16 @@ { - "name": "deplacement-covid-19", + "name": "deplacement-vers-france-covid-19", "version": "0.0.1", - "description": "GĂ©nĂ©rateur d'attestation de dĂ©placement dĂ©rogatoire'", + "description": "GĂ©nĂ©rateur d'attestation de dĂ©placement international dĂ©rogatoire vers la France", "main": "certificate.js", "scripts": { - "lint": "eslint ./*.js", - "start": "cross-env VERSION=${VERSION:-localversion} parcel --public-url ${PUBLIC_URL:-/deplacement-covid-19} ./src/index.html", + "lint": "eslint src/*.js", + "format": "npm run lint -- --fix", + "start": "cross-env VERSION=${VERSION:-localversion} parcel --public-url ${PUBLIC_URL:-/deplacement-vers-france-covid-19} ./src/index.html", "clean:dist": "rimraf dist", "prebuild": "run-s lint clean:dist", - "build": "parcel build --public-url ${PUBLIC_URL:-/deplacement-covid-19} ./src/index.html ./src/robots.txt ./src/sitemap.xml", - "postbuild": "PUBLIC_URL=${PUBLIC_URL:-/deplacement-covid-19} react-snap", + "build": "parcel build --public-url ${PUBLIC_URL:-/deplacement-vers-france-covid-19} ./src/index.html ./src/index-en.html ./src/robots.txt ./src/sitemap.xml", + "postbuild": "PUBLIC_URL=${PUBLIC_URL:-/deplacement-vers-france-covid-19} react-snap", "preserve": "npm run build", "serve": "serve dist", "serve:dist": "serve dist" @@ -19,7 +20,7 @@ "url": "git+https://github.com/lab-mi/deplacement-covid-19" }, "keywords": [], - "author": "", + "author": "MinistĂšre de l'IntĂ©rieur", "license": "MIT", "bugs": { "url": "https://github.com/lab-mi/deplacement-covid-19/issues" @@ -55,7 +56,8 @@ "qrcode": "^1.4.4" }, "browserslist": [ - "last 5 versions" + "last 5 versions", + "ios_saf >= 7" ], "reactSnap": { "source": "dist", @@ -73,6 +75,9 @@ "strategy": "default", "clearDist": false, "templatedURLs": { - "./": ["index.html"] - } } + "./": [ + "index.html" + ] + } + } } diff --git a/src/07-04-20-Attestation-etranger-metropole-FR.pdf b/src/07-04-20-Attestation-etranger-metropole-FR.pdf new file mode 100644 index 0000000..06bee02 Binary files /dev/null and b/src/07-04-20-Attestation-etranger-metropole-FR.pdf differ diff --git a/src/Attestation_deplacement_International_EN-1.pdf b/src/Attestation_deplacement_International_EN-1.pdf new file mode 100644 index 0000000..9fd7b29 Binary files /dev/null and b/src/Attestation_deplacement_International_EN-1.pdf differ diff --git a/src/Flag_of_France.svg b/src/Flag_of_France.svg new file mode 100644 index 0000000..681f049 --- /dev/null +++ b/src/Flag_of_France.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/src/certificate-en.js b/src/certificate-en.js new file mode 100644 index 0000000..8d99a5c --- /dev/null +++ b/src/certificate-en.js @@ -0,0 +1,328 @@ +import 'bootstrap/dist/css/bootstrap.min.css' + +import './main.css' + +import { PDFDocument, StandardFonts } from 'pdf-lib' +import QRCode from 'qrcode' +import { library, dom } from '@fortawesome/fontawesome-svg-core' +import { faEye, faFilePdf } from '@fortawesome/free-solid-svg-icons' + +import './check-updates' +import { $, $$ } from './dom-utils' +import pdfBase from './Attestation_deplacement_International_EN-1.pdf' + +library.add(faEye, faFilePdf) + +dom.watch() + +$('#radio-language-fr').addEventListener('click', async event => { + window.location.href = '/index' +}) + +const generateQR = async text => { + try { + const opts = { + errorCorrectionLevel: 'M', + type: 'image/png', + quality: 0.92, + margin: 1, + } + return await QRCode.toDataURL(text, opts) + } catch (err) { + console.error(err) + } +} + +function saveProfile () { + for (const field of $$('#form-profile input')) { + localStorage.setItem(field.id.substring('field-'.length), field.value) + } +} + +function getProfile () { + const fields = {} + for (let i = 0; i < localStorage.length; i++) { + const name = localStorage.key(i) + fields[name] = localStorage.getItem(name) + } + return fields +} + +function idealFontSize (font, text, maxWidth, minSize, defaultSize) { + let currentSize = defaultSize + let textWidth = font.widthOfTextAtSize(text, defaultSize) + + while (textWidth > maxWidth && currentSize > minSize) { + textWidth = font.widthOfTextAtSize(text, --currentSize) + } + + return (textWidth > maxWidth) ? null : currentSize +} + +async function generatePdf (profile, typeNationality, reasons) { + const creationDate = new Date().toLocaleDateString('fr-FR') + const creationHour = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }).replace(':', 'h') + + const { lastname, firstname, birthday, nationality, address, zipcode, town, country } = profile + + const data = [ + `Cree le: ${creationDate} a ${creationHour}`, + `Nom: ${lastname}`, + `Prenom: ${firstname}`, + `Naissance: ${birthday} (${nationality})`, + `Adresse: ${address} ${zipcode} ${town} ${country}`, + 'Sortie: N/A', + `Motifs: ${typeNationality}-${reasons}`, + ].join(';\n ') + + const existingPdfBytes = await fetch(pdfBase).then(res => res.arrayBuffer()) + + const pdfDoc = await PDFDocument.load(existingPdfBytes) + const page1 = pdfDoc.getPages()[0] + + const font = await pdfDoc.embedFont(StandardFonts.Helvetica) + const drawText = (text, x, y, size = 11) => { + page1.drawText(text, { x, y, size, font }) + } + + drawText(`${firstname} ${lastname}`, 125, 590) + drawText(birthday, 125, 567) + drawText(nationality, 125, 545) + drawText(`${address} ${zipcode}`, 127, 527) + drawText(`${town}, ${country}`, 127, 505) + + if (typeNationality === 'tiers') { + if (reasons.includes('residence')) { + drawText('x', 49, 449, 19) + } + if (reasons.includes('transit')) { + drawText('x', 49, 412, 19) + } + if (reasons.includes('prof._sante')) { + drawText('x', 49, 385, 19) + } + if (reasons.includes('marchandises')) { + drawText('x', 49, 370, 19) + } + if (reasons.includes('equipage')) { + drawText('x', 49, 354, 19) + } + if (reasons.includes('diplomatique')) { + drawText('x', 49, 339, 19) + } + if (reasons.includes('frontalier')) { + drawText('x', 49, 313, 19) + } + } + + if (typeNationality === 'eu') { + if (reasons.includes('resident')) { + drawText('x', 49, 276, 19) + } + if (reasons.includes('transit')) { + drawText('x', 49, 263, 19) + } + if (reasons.includes('prof._sante')) { + drawText('x', 49, 250, 19) + } + if (reasons.includes('marchandises')) { + drawText('x', 49, 238, 19) + } + if (reasons.includes('equipage')) { + drawText('x', 49, 225, 19) + } + if (reasons.includes('diplomatique')) { + drawText('x', 49, 212, 19) + } + if (reasons.includes('frontalier')) { + drawText('x', 49, 189, 19) + } + } + if (typeNationality === 'fr') { + drawText('x', 49, 162, 19) + } + let locationSize = idealFontSize(font, profile.town, 83, 7, 11) + + if (!locationSize) { + alert('Le nom de la ville risque de ne pas ĂȘtre affichĂ© correctement en raison de sa longueur. ' + + 'Essayez d\'utiliser des abrĂ©viations ("Saint" en "St." par exemple) quand cela est possible.') + locationSize = 7 + } + + // Fait Ă  : + drawText(profile.town, 395, 142, locationSize) + // Le + drawText(`${new Date().toLocaleDateString('fr-FR', { month: 'numeric', day: 'numeric' })}`, 488, 142) + + const generatedQR = await generateQR(data) + + const qrImage = await pdfDoc.embedPng(generatedQR) + + page1.drawImage(qrImage, { + x: 450, + y: 572, + width: 100, + height: 100, + }) + + pdfDoc.addPage() + const page2 = pdfDoc.getPages()[1] + page2.drawImage(qrImage, { + x: 50, + y: page2.getHeight() - 350, + width: 300, + height: 300, + }) + + const pdfBytes = await pdfDoc.save() + + return new Blob([pdfBytes], { type: 'application/pdf' }) +} + +function downloadBlob (blob, fileName) { + const link = document.createElement('a') + const url = URL.createObjectURL(blob) + link.href = url + link.download = fileName + document.body.appendChild(link) + link.click() +} + +function getAndSaveReasons () { + const values = $$('input[name="field-reason"]:checked') + .map(x => x.value) + .join('-') + localStorage.setItem('reasons', values) + return values +} + +function getAndSaveTypeNationality () { + const typeNationality = $$('input[name="field-type-nationality"]:checked') + .map(x => x.value) + .join('-') + localStorage.setItem('typeNationality', typeNationality) + + return typeNationality +} + +// see: https://stackoverflow.com/a/32348687/1513045 +function isFacebookBrowser () { + const ua = navigator.userAgent || navigator.vendor || window.opera + return ua.includes('FBAN') || ua.includes('FBAV') +} + +if (isFacebookBrowser()) { + $('#alert-facebook').value = 'ATTENTION !! Vous utilisez actuellement le navigateur Facebook, ce gĂ©nĂ©rateur ne fonctionne pas correctement au sein de ce navigateur ! Merci d\'ouvrir Chrome sur Android ou bien Safari sur iOS.' + $('#alert-facebook').classList.remove('d-none') +} + +function addSlash () { + $('#field-birthday').value = $('#field-birthday').value.replace(/^(\d{2})$/g, '$1/') + $('#field-birthday').value = $('#field-birthday').value.replace(/^(\d{2})\/(\d{2})$/g, '$1/$2/') + $('#field-birthday').value = $('#field-birthday').value.replace(/\/\//g, '/') +} + +$('#field-birthday').onkeyup = function () { + const key = event.keyCode || event.charCode + if (key === 8 || key === 46) { + return false + } else { + addSlash() + return false + } +} + +const snackbar = $('#snackbar') + +$('#generate-btn').addEventListener('click', async event => { + event.preventDefault() + + saveProfile() + const typeNationality = getAndSaveTypeNationality() + let reasons + if (typeNationality === 'fr') { + reasons = 'N/A' + } else { + reasons = getAndSaveReasons() + } + + const pdfBlob = await generatePdf(getProfile(), typeNationality, reasons) + localStorage.clear() + const creationDate = new Date().toLocaleDateString('fr-CA') + const creationHour = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }).replace(':', '-') + downloadBlob(pdfBlob, `attestation-${creationDate}_${creationHour}.pdf`) + + snackbar.classList.remove('d-none') + setTimeout(() => snackbar.classList.add('show'), 100) + + setTimeout(function () { + snackbar.classList.remove('show') + setTimeout(() => snackbar.classList.add('d-none'), 500) + }, 6000) +}) + +$$('input').forEach(input => { + const exempleElt = input.parentNode.parentNode.querySelector('.exemple') + if (input.placeholder && exempleElt) { + input.addEventListener('input', (event) => { + if (input.value) { + exempleElt.innerHTML = 'ex. : ' + input.placeholder + } else { + exempleElt.innerHTML = '' + } + }) + } +}) + +const conditions = { + '#field-firstname': { + condition: 'length', + }, + '#field-lastname': { + condition: 'length', + }, + '#field-birthday': { + condition: 'pattern', + pattern: /^([0][1-9]|[1-2][0-9]|30|31)\/([0][1-9]|10|11|12)\/(19[0-9][0-9]|20[0-1][0-9]|2020)/g, + }, + '#field-nationality': { + condition: 'length', + }, + '#field-address': { + condition: 'length', + }, + '#field-town': { + condition: 'length', + }, + '#field-zipcode': { + condition: 'lenght', + }, + '#field-country': { + condition: 'length', + }, +} + +Object.keys(conditions).forEach(field => { + $(field).addEventListener('input', () => { + if (conditions[field].condition === 'pattern') { + const pattern = conditions[field].pattern + if ($(field).value.match(pattern)) { + $(field).setAttribute('aria-invalid', 'false') + } else { + $(field).setAttribute('aria-invalid', 'true') + } + } + if (conditions[field].condition === 'length') { + if ($(field).value.length > 0) { + $(field).setAttribute('aria-invalid', 'false') + } else { + $(field).setAttribute('aria-invalid', 'true') + } + } + }) +}) + +function addVersion () { + document.getElementById('version').innerHTML = `${new Date().getFullYear()} - ${process.env.VERSION}` +} +addVersion() diff --git a/src/certificate.js b/src/certificate.js index 46f0778..ed363f7 100644 --- a/src/certificate.js +++ b/src/certificate.js @@ -9,17 +9,19 @@ import { faEye, faFilePdf } from '@fortawesome/free-solid-svg-icons' import './check-updates' import { $, $$ } from './dom-utils' -import pdfBase from './certificate.pdf' +import pdfBase from './07-04-20-Attestation-etranger-metropole-FR.pdf' library.add(faEye, faFilePdf) dom.watch() -var year, month, day +$('#radio-language-en').addEventListener('click', async event => { + window.location.href = '/index-en' +}) const generateQR = async text => { try { - var opts = { + const opts = { errorCorrectionLevel: 'M', type: 'image/png', quality: 0.92, @@ -31,39 +33,9 @@ const generateQR = async text => { } } -function pad (str) { - return String(str).padStart(2, '0') -} - -function setDateNow (date) { - year = date.getFullYear() - month = pad(date.getMonth() + 1) // Les mois commencent Ă  0 - day = pad(date.getDate()) -} - -document.addEventListener('DOMContentLoaded', setReleaseDateTime) - -function setReleaseDateTime () { - const loadedDate = new Date() - setDateNow(loadedDate) - const releaseDateInput = document.querySelector('#field-datesortie') - releaseDateInput.value = `${year}-${month}-${day}` - - const hour = pad(loadedDate.getHours()) - const minute = pad(loadedDate.getMinutes()) - - const releaseTimeInput = document.querySelector('#field-heuresortie') - releaseTimeInput.value = `${hour}:${minute}` -} - function saveProfile () { for (const field of $$('#form-profile input')) { - if (field.id === 'field-datesortie') { - var dateSortie = field.value.split('-') - localStorage.setItem(field.id.substring('field-'.length), `${dateSortie[2]}/${dateSortie[1]}/${dateSortie[0]}`) - } else { - localStorage.setItem(field.id.substring('field-'.length), field.value) - } + localStorage.setItem(field.id.substring('field-'.length), field.value) } } @@ -87,23 +59,21 @@ function idealFontSize (font, text, maxWidth, minSize, defaultSize) { return (textWidth > maxWidth) ? null : currentSize } -async function generatePdf (profile, reasons) { +async function generatePdf (profile, typeNationality, reasons) { const creationDate = new Date().toLocaleDateString('fr-FR') const creationHour = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }).replace(':', 'h') - const { lastname, firstname, birthday, lieunaissance, address, zipcode, town, datesortie, heuresortie } = profile - const releaseHours = String(heuresortie).substring(0, 2) - const releaseMinutes = String(heuresortie).substring(3, 5) + const { lastname, firstname, birthday, nationality, address, zipcode, town, country } = profile const data = [ `Cree le: ${creationDate} a ${creationHour}`, `Nom: ${lastname}`, `Prenom: ${firstname}`, - `Naissance: ${birthday} a ${lieunaissance}`, - `Adresse: ${address} ${zipcode} ${town}`, - `Sortie: ${datesortie} a ${releaseHours}h${releaseMinutes}`, - `Motifs: ${reasons}`, - ].join('; ') + `Naissance: ${birthday} (${nationality})`, + `Adresse: ${address} ${zipcode} ${town} ${country}`, + 'Sortie: N/A', + `Motifs: ${typeNationality}-${reasons}`, + ].join(';\n ') const existingPdfBytes = await fetch(pdfBase).then(res => res.arrayBuffer()) @@ -115,31 +85,61 @@ async function generatePdf (profile, reasons) { page1.drawText(text, { x, y, size, font }) } - drawText(`${firstname} ${lastname}`, 123, 686) - drawText(birthday, 123, 661) - drawText(lieunaissance, 92, 638) - drawText(`${address} ${zipcode} ${town}`, 134, 613) + drawText(`${firstname} ${lastname}`, 125, 590) + drawText(birthday, 125, 567) + drawText(nationality, 125, 545) + drawText(`${address} ${zipcode}`, 127, 527) + drawText(`${town}, ${country}`, 127, 505) - if (reasons.includes('travail')) { - drawText('x', 76, 527, 19) + if (typeNationality === 'tiers') { + if (reasons.includes('residence')) { + drawText('x', 49, 449, 19) + } + if (reasons.includes('transit')) { + drawText('x', 49, 412, 19) + } + if (reasons.includes('prof._sante')) { + drawText('x', 49, 385, 19) + } + if (reasons.includes('marchandises')) { + drawText('x', 49, 370, 19) + } + if (reasons.includes('equipage')) { + drawText('x', 49, 354, 19) + } + if (reasons.includes('diplomatique')) { + drawText('x', 49, 328, 19) + } + if (reasons.includes('frontalier')) { + drawText('x', 49, 302, 19) + } } - if (reasons.includes('courses')) { - drawText('x', 76, 478, 19) + + if (typeNationality === 'eu') { + if (reasons.includes('resident')) { + drawText('x', 49, 265, 19) + } + if (reasons.includes('transit')) { + drawText('x', 49, 249, 19) + } + if (reasons.includes('prof._sante')) { + drawText('x', 49, 234, 19) + } + if (reasons.includes('marchandises')) { + drawText('x', 49, 218, 19) + } + if (reasons.includes('equipage')) { + drawText('x', 49, 202, 19) + } + if (reasons.includes('diplomatique')) { + drawText('x', 49, 177, 19) + } + if (reasons.includes('frontalier')) { + drawText('x', 49, 150, 19) + } } - if (reasons.includes('sante')) { - drawText('x', 76, 436, 19) - } - if (reasons.includes('famille')) { - drawText('x', 76, 400, 19) - } - if (reasons.includes('sport')) { - drawText('x', 76, 345, 19) - } - if (reasons.includes('judiciaire')) { - drawText('x', 76, 298, 19) - } - if (reasons.includes('missions')) { - drawText('x', 76, 260, 19) + if (typeNationality === 'fr') { + drawText('x', 49, 127, 19) } let locationSize = idealFontSize(font, profile.town, 83, 7, 11) @@ -149,26 +149,18 @@ async function generatePdf (profile, reasons) { locationSize = 7 } - drawText(profile.town, 111, 226, locationSize) - - if (reasons !== '') { - // Date sortie - drawText(`${profile.datesortie}`, 92, 200) - drawText(releaseHours, 200, 201) - drawText(releaseMinutes, 220, 201) - } - - // Date crĂ©ation - drawText('Date de crĂ©ation:', 464, 150, 7) - drawText(`${creationDate} Ă  ${creationHour}`, 455, 144, 7) + // Fait Ă  : + drawText(profile.town, 388, 107, locationSize) + // Le + drawText(`${new Date().toLocaleDateString('fr-FR', { month: 'numeric', day: 'numeric' })}`, 488, 107) const generatedQR = await generateQR(data) const qrImage = await pdfDoc.embedPng(generatedQR) page1.drawImage(qrImage, { - x: page1.getWidth() - 170, - y: 155, + x: 450, + y: 572, width: 100, height: 100, }) @@ -189,7 +181,7 @@ async function generatePdf (profile, reasons) { function downloadBlob (blob, fileName) { const link = document.createElement('a') - var url = URL.createObjectURL(blob) + const url = URL.createObjectURL(blob) link.href = url link.download = fileName document.body.appendChild(link) @@ -204,6 +196,15 @@ function getAndSaveReasons () { return values } +function getAndSaveTypeNationality () { + const typeNationality = $$('input[name="field-type-nationality"]:checked') + .map(x => x.value) + .join('-') + localStorage.setItem('typeNationality', typeNationality) + + return typeNationality +} + // see: https://stackoverflow.com/a/32348687/1513045 function isFacebookBrowser () { const ua = navigator.userAgent || navigator.vendor || window.opera @@ -237,12 +238,19 @@ $('#generate-btn').addEventListener('click', async event => { event.preventDefault() saveProfile() - const reasons = getAndSaveReasons() - const pdfBlob = await generatePdf(getProfile(), reasons) + const typeNationality = getAndSaveTypeNationality() + let reasons + if (typeNationality === 'fr') { + reasons = 'N/A' + } else { + reasons = getAndSaveReasons() + } + + const pdfBlob = await generatePdf(getProfile(), typeNationality, reasons) localStorage.clear() const creationDate = new Date().toLocaleDateString('fr-CA') const creationHour = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }).replace(':', '-') - downloadBlob(pdfBlob, `attestation-${creationDate}_${creationHour}.pdf`) + downloadBlob(pdfBlob, `attestation-${creationDate}_${creationHour}.pdf`) snackbar.classList.remove('d-none') setTimeout(() => snackbar.classList.add('show'), 100) @@ -275,9 +283,9 @@ const conditions = { }, '#field-birthday': { condition: 'pattern', - pattern: /^([0][1-9]|[1-2][0-9]|30|31)\/([0][1-9]|10|11|12)\/(19[0-9][0-9]|20[0-1][0-9]|2020)/g + pattern: /^([0][1-9]|[1-2][0-9]|30|31)\/([0][1-9]|10|11|12)\/(19[0-9][0-9]|20[0-1][0-9]|2020)/g, }, - '#field-lieunaissance': { + '#field-nationality': { condition: 'length', }, '#field-address': { @@ -287,34 +295,28 @@ const conditions = { condition: 'length', }, '#field-zipcode': { - condition: 'pattern', - pattern: /\d{5}/g + condition: 'lenght', }, - '#field-datesortie': { - condition: 'pattern', - pattern: /\d{4}-\d{2}-\d{2}/g + '#field-country': { + condition: 'length', }, - '#field-heuresortie': { - condition: 'pattern', - pattern: /\d{2}:\d{2}/g - } } Object.keys(conditions).forEach(field => { $(field).addEventListener('input', () => { - if (conditions[field].condition == 'pattern') { - const pattern = conditions[field].pattern; + if (conditions[field].condition === 'pattern') { + const pattern = conditions[field].pattern if ($(field).value.match(pattern)) { - $(field).setAttribute('aria-invalid', "false"); + $(field).setAttribute('aria-invalid', 'false') } else { - $(field).setAttribute('aria-invalid', "true"); + $(field).setAttribute('aria-invalid', 'true') } } - if (conditions[field].condition == 'length') { + if (conditions[field].condition === 'length') { if ($(field).value.length > 0) { - $(field).setAttribute('aria-invalid', "false"); + $(field).setAttribute('aria-invalid', 'false') } else { - $(field).setAttribute('aria-invalid', "true"); + $(field).setAttribute('aria-invalid', 'true') } } }) diff --git a/src/favicons/apple-touch-icon-120x120-precomposed.png b/src/favicons/apple-touch-icon-120x120-precomposed.png deleted file mode 100644 index b40c2e2..0000000 Binary files a/src/favicons/apple-touch-icon-120x120-precomposed.png and /dev/null differ diff --git a/src/favicons/apple-touch-icon-120x120.png b/src/favicons/apple-touch-icon-120x120.png deleted file mode 100644 index b40c2e2..0000000 Binary files a/src/favicons/apple-touch-icon-120x120.png and /dev/null differ diff --git a/src/favicons/apple-touch-icon-precomposed.png b/src/favicons/apple-touch-icon-precomposed.png deleted file mode 100644 index 7e93fe3..0000000 Binary files a/src/favicons/apple-touch-icon-precomposed.png and /dev/null differ diff --git a/src/favicons/apple-touch-icon.png b/src/favicons/apple-touch-icon.png index 7e93fe3..1874633 100644 Binary files a/src/favicons/apple-touch-icon.png and b/src/favicons/apple-touch-icon.png differ diff --git a/src/favicons/favicon.ico b/src/favicons/favicon.ico deleted file mode 100644 index 26cee48..0000000 Binary files a/src/favicons/favicon.ico and /dev/null differ diff --git a/src/favicons/site.webmanifest b/src/favicons/site.webmanifest index 6b1f870..2dcee57 100644 --- a/src/favicons/site.webmanifest +++ b/src/favicons/site.webmanifest @@ -14,26 +14,6 @@ "src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" - }, - { - "src": "apple-touch-icon.png", - "sizes": "180x180", - "type": "image/png" - }, - { - "src": "apple-touch-icon-precomposed.png", - "sizes": "180x180", - "type": "image/png" - }, - { - "src": "apple-touch-icon-120x120.png", - "sizes": "120x120", - "type": "image/png" - }, - { - "src": "apple-touch-icon-120x120-precomposed.png", - "sizes": "120x120", - "type": "image/png" } ], "orientation": "portrait-primary", diff --git a/src/flag EN.svg b/src/flag EN.svg new file mode 100644 index 0000000..dc69fa0 --- /dev/null +++ b/src/flag EN.svg @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/index-en.html b/src/index-en.html new file mode 100644 index 0000000..356b20f --- /dev/null +++ b/src/index-en.html @@ -0,0 +1,378 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + COVID-19 – Generator of international travel certificate to mainland France + + + +
+ + +
+ +
+

Fill online your digital certificate :

+

All fields are mandatory.

+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ +
+ + +
+

+
+ +
+ You are : + +
+ + + +
+ Choose the reason : +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ + + +
+ Choose the reason : +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+ +

+ +

+ +
+ The certificate is downloaded on your terminal. +
+
+ + +
+
+

+ [1] United Kingdom, Iceland, Liechtenstein, Norway, Andorra, Monaco, Switzerland, San Marino, Holy See.
+ [2] European Union, United Kingdom, Iceland, Liechtenstein, Norway, Andorra, Monaco, Switzerland, San Marino and Holy See nationals (2004/38/CE directive). +

+ +

+ The source code of this service is available for consultation on GitHub. +

+

+ Ministry of the Interior - DNUM - SDIT +

+ logo dnum +
+
+ + +
+ A new version is available. Click on the button to get it. +

+ +

+
+ + + + diff --git a/src/index.html b/src/index.html index 64408ed..e6fa25d 100644 --- a/src/index.html +++ b/src/index.html @@ -6,19 +6,19 @@ - + - + - + - - - - + + + + @@ -26,25 +26,50 @@ - COVID-19 – GĂ©nĂ©rateur d'attestation de dĂ©placement dĂ©rogatoire + COVID-19 – GĂ©nĂ©rateur d'attestation de dĂ©placement international dĂ©rogatoire vers la France mĂ©tropolitaine
@@ -55,7 +80,7 @@

Remplissez en ligne votre attestation numérique :

Tous les champs sont obligatoires.

- +
@@ -67,14 +92,13 @@ autocomplete="given-name" placeholder="Jean" required - autofocus aria-invalid="true" >

- +
@@ -87,13 +111,12 @@ placeholder="Dupont" aria-invalid="true" required - autofocus >

- +
@@ -114,24 +137,25 @@

- +
- +

- +
@@ -141,15 +165,15 @@ id="field-address" name="address" aria-invalid="true" - autocomplete="address-line1" - placeholder="999 avenue de france" + autocomplete="address-line1" + placeholder="999 avenue de Belgique" required >

- +
@@ -160,91 +184,151 @@ name="town" autocomplete="address-level1" aria-invalid="true" - placeholder="Paris" + placeholder="Bruxelles" required >

- +

- -
- Choisissez le ou les motif(s) de sortie -
- - + +
+ +
+ +
- +

+
+ +
+ Vous ĂȘtes : +
- - + + + +
+ Choisissez le motif : +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
- +
- - + + + +
+ Choisissez le motif +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
- +
- - -
- -
- - -
-
- - -
-
- - + +
+
- -
- -
- - -
-
- -
- -
- - -
-
- - +

@@ -253,14 +337,21 @@ L'attestation est téléchargée sur votre appareil.
+ +
+

+ [1] Royaume-Uni, Islande, Liechtenstein, NorvĂšge, Andorre, Monaco, Suisse, Saint-Marin, Saint SiĂšge.
+ [2] Ressortissants de l’Union europĂ©enne et ressortissants britanniques, ainsi que les ressortissants islandais, liechtensteinois, norvĂ©giens, andorrans, monĂ©gasques, suisses, saint-marinais, citoyens du Saint SiĂšge (directive 2004/38/CE). +

+

Le code source de ce service est consultable sur Github.

MinistÚre de l'Intérieur - DNUM - SDIT -

+

logo dnum
@@ -268,11 +359,11 @@ - +
Une nouvelle version est disponible. Cliquer sur le bouton pour l'obtenir. diff --git a/src/main.css b/src/main.css index 76ce46b..06fa10e 100644 --- a/src/main.css +++ b/src/main.css @@ -29,14 +29,14 @@ h1 { } /* Small devices (landscape phones, 576px and up) */ -@media (min-width: 576px) { +@media (min-width: 576px) { h1 { font-size: 2.5em; } } /* Medium devices (tablets, 768px and up) */ -@media (min-width: 768px) { +@media (min-width: 768px) { h1 { font-size: 3em; } @@ -46,6 +46,13 @@ svg { height: 1em; } +ul.flex-justify { + display: flex; + justify-content:space-between; + padding: 0; + margin: 0; + } + h1.flex.flex-wrap { display: flex; flex-wrap: wrap; @@ -83,6 +90,11 @@ a { transform: translateY(-2px); } +#form-profile .form-nationality-label { + font-weight: 800; + transform: translateY(-2px); +} + #form-generate .form-check { margin: 10px; } @@ -155,6 +167,27 @@ input:valid+span:after { width: 50%; } +fieldset .form-language { + display: flex; + align-items: right; +} + +.legend-language { + text-align: left; + font-size: 1rem; + color: #000000; +} + +.form-language .form-check-label { + font-size: 1rem; + font-weight: bold; + color: #000000; +} + +.flags { + width: 20px; + height: 15px; +} .covid-title { display: flex; @@ -185,11 +218,10 @@ input:valid+span:after { .text-alert{ text-align: left; + font-style: italic; color: #000000; } - - .btn-attestation { padding: 0.9em; font-size: 1.2em; @@ -308,13 +340,13 @@ input:valid+span:after { text-decoration: none; color: #ffffff; } - + } em { font-size: .8rem; } } - + .titre-2 { text-align: left; font-size: 1.5rem; @@ -401,3 +433,22 @@ input:valid+span:after { left: 50%; transform: translateX(-50%); } + +.control:not(:checked) ~ .conditional, +#radio-fr:not(:checked) ~ .conditional, +#radio-eu:not(:checked) ~ .conditional, +#radio-tiers:not(:checked) ~ .conditional { + clip: rect(0 0 0 0); + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; +} + +[id^="footnote"] { + margin: 30px auto; + max-width: 400px; + font-size: 0.8em; + color: #000000; + +} diff --git a/src/robots.txt b/src/robots.txt index 423bf62..5306e97 100644 --- a/src/robots.txt +++ b/src/robots.txt @@ -1 +1 @@ -Sitemap: https://media.interieur.gouv.fr/deplacement-covid-19/sitemap.xml +Sitemap: https://media.interieur.gouv.fr/deplacement-vers-france-covid-19/sitemap.xml diff --git a/src/sitemap.xml b/src/sitemap.xml index c7b756d..f41161b 100644 --- a/src/sitemap.xml +++ b/src/sitemap.xml @@ -8,20 +8,20 @@ - https://media.interieur.gouv.fr/deplacement-covid-19/ + https://media.interieur.gouv.fr/deplacement-vers-france-covid-19/ 2020-04-06T04:22:03+00:00 1.00 - https://media.interieur.gouv.fr/deplacement-covid-19/confidentialite.html + https://media.interieur.gouv.fr/deplacement-vers-france-covid-19/confidentialite.html 2020-04-06T04:22:03+00:00 0.80 - https://media.interieur.gouv.fr/deplacement-covid-19/index.html + https://media.interieur.gouv.fr/deplacement-vers-france-covid-19/index.html 2020-04-06T04:22:03+00:00 0.64 - \ No newline at end of file +