🎉 Première version du projet

This commit is contained in:
odjpromi 2020-10-30 10:12:32 +01:00
commit bdad4c95c2
51 changed files with 27154 additions and 0 deletions

10
.babelrc Normal file
View file

@ -0,0 +1,10 @@
{
"presets": [
"@babel/preset-env"
],
"plugins": [
["@babel/plugin-transform-runtime", {
"regenerator": true
}]
]
}

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true
end_of_line = lf
[*.html]
indent_style = space
indent_size = 2
[*.{js,json,yml}]
indent_style = space
indent_size = 2
[*.js]
quote_type = single

33
.eslintrc.js Normal file
View file

@ -0,0 +1,33 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
},
extends: ["standard"],
rules: {
"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: [
{
files: ["**/__tests__/*.js", "**/tests/unit/**/*.spec.js"],
env: {
jest: true
}
}
],
parserOptions: {
parser: "babel-eslint"
},
overrides: [
{
files: ["**/__tests__/*.js", "**/tests/unit/**/*.spec.js"],
env: {
jest: true
}
}
]
};

213
.gitignore vendored Normal file
View file

@ -0,0 +1,213 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,code,linux,vim,windows,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=node,code,linux,vim,windows,macos
### Code ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
### Vim ###
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,code,linux,vim,windows,macos

4
.prettierrc.js Normal file
View file

@ -0,0 +1,4 @@
module.exports = {
singleQuote: true,
semi: false
};

20
LICENCE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2020 Ministère de l'intérieur
Copyright (c) 2020 Johann Pardanaud
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

44
README.md Normal file
View file

@ -0,0 +1,44 @@
# Générateur de certificat de déplacement
## Développer
### Installer le projet
```console
git clone https://github.com/LAB-MI/attestation-deplacement-derogatoire-q4-2020.git
cd attestation-deplacement-derogatoire-q4-2020
npm i
npm start
```
## Générer et tester le code de production
### Tester le code de production en local
#### Générer le code de production pour tester que le build fonctionne en entier
```console
npm run build:dev
```
#### Tester le code de production en local
```console
npx serve dist
```
Et visiter http://localhost:5000
Le code à déployer sera le contenu du dossier `dist`
## Crédits
Ce projet a été réalisé à partir d'un fork du dépôt [deplacement-covid-19](https://github.com/nesk/deplacement-covid-19) de lui-même réalisé à partir d'un fork du dépôt [covid-19-certificate](https://github.com/nesk/covid-19-certificate) de [Johann Pardanaud](https://github.com/nesk).
Les projets open source suivants ont été utilisés pour le développement de ce
service :
- [PDF-LIB](https://pdf-lib.js.org/)
- [qrcode](https://github.com/soldair/node-qrcode)
- [Bootstrap](https://getbootstrap.com/)
- [Font Awesome](https://fontawesome.com/license)

24761
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

94
package.json Normal file
View file

@ -0,0 +1,94 @@
{
"name": "attestation-couvre-feu",
"version": "1.0.3",
"description": "Générateur d'attestation de déplacement dérogatoire'",
"main": "certificate.js",
"scripts": {
"lint": "eslint src/**/*.js",
"preformat": "prettier --write \"src/**/*.js\"",
"format": "npm run lint -- --fix",
"start": "cross-env VERSION=localversion parcel ./src/index.html",
"start:grid": "cross-env VERSION=localversion parcel ./src/grid.html",
"clean:dist": "rimraf dist",
"prebuild": "run-p lint clean:dist",
"build:simple": "cross-env-shell VERSION=$npm_package_version parcel build --public-url $PUBLIC_URL ./src/index.html ./src/robots.txt ./src/sitemap.xml",
"build": "cross-env npm run build:simple",
"postbuild": "cross-env-shell react-snap",
"prebuild:ci": "run-p lint clean:dist",
"build:ci": "cross-env npm run build:simple",
"postbuild:ci": "cross-env-shell react-snap",
"build:dev": "cross-env PUBLIC_URL='/' npm run build:simple",
"preserve": "npm run build",
"serve": "serve dist",
"serve:dist": "serve dist"
},
"repository": {
"type": "git",
"url": "git+https://github.com/lab-mi/attestation-deplacement-derogatoire-q4-2020"
},
"keywords": [],
"author": "",
"license": "MIT",
"bugs": {
"url": "https://github.com/lab-mi/attestation-deplacement-derogatoire-q4-2020/issues"
},
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/plugin-transform-runtime": "^7.12.1",
"babel-eslint": "^10.1.0",
"copy-and-watch": "^0.1.5",
"cross-env": "^7.0.2",
"eslint": "^7.12.1",
"eslint-config-standard": "^16.0.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.2",
"npm-run-all": "^4.1.5",
"parcel-bundler": "^1.12.4",
"parcel-plugin-sw-cache": "^0.3.1",
"postcss-current-selector": "0.0.3",
"postcss-nested": "^4.2.3",
"postcss-nested-ancestors": "^2.0.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.1.2",
"react-snap": "^1.23.0",
"rimraf": "^3.0.2",
"serve": "^11.3.2"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"bootstrap": "^4.5.3",
"pdf-lib": "^1.11.2",
"qrcode": "^1.4.4"
},
"browserslist": [
"last 5 versions"
],
"reactSnap": {
"source": "dist",
"minifyHtml": {
"collapseWhitespace": false,
"removeComments": false
},
"puppeteerArgs": [
"--no-sandbox",
"--disable-setuid-sandbox"
]
},
"cache": {
"globPatterns": [
"**/*.{html,js,css,jpg,png,pdf,svg,eot,ttf,woff,woff2}"
],
"disablePlugin": false,
"inDev": true,
"strategy": "default",
"clearDist": false,
"templatedURLs": {
"./": [
"index.html"
]
}
}
}

11
postcss.config.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
plugins: {
autoprefixer: {
grid: true,
},
'postcss-preset-env': {},
'postcss-nested-ancestors': {},
'postcss-nested': {},
'postcss-current-selector': {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

BIN
src/certificate.pdf Normal file

Binary file not shown.

195
src/confidentialite.html Normal file
View file

@ -0,0 +1,195 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="msapplication-TileColor" content="#603cba" />
<meta name="msapplication-config" content="./favicons/browserconfig.xml" />
<meta name="theme-color" content="#ffffff" />
<link rel="apple-touch-icon" sizes="180x180" href="./favicons/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="./favicons/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./favicons/favicon-16x16.png" />
<link rel="manifest" href="./favicons/site.webmanifest" />
<link rel="mask-icon" href="./favicons/safari-pinned-tab.svg" color="#21bf73" />
<title>COVID-19 Générateur d'attestation de déplacement</title>
</head>
<body class="confidentialite">
<div class="container">
<header class="wrapper">
<picture>
<source srcset="/MIN_Interieur_RVB_dark.svg" media="(prefers-color-scheme: dark)">
<img src="/MIN_Interieur_RVB.svg" alt="Ministère de l'intérieur. Liberté, égalité, fraternité." class="logo" role="presentation" aria-hidden="true">
</picture>
<div class="header-content">
<h1 class="flex flex-wrap">
<div class="covid-title">
COVID-19
</div>
<div class="covid-subtitle">
Générateur d'attestation de&nbsp;déplacement
</div>
</h1>
</div>
</header>
<div class="wrapper">
<h2 class="titre-2">Politique de confidentialité</h2>
<h3 class="titre-3">Collecte des données personnelles</h3>
<p>
Les informations saisies dans ce générateur dattestation de
déplacement ne font lobjet daucune collecte par le ministère
de lIntérieur. Ces données personnelles<sup>1</sup> sont
exclusivement stockées dans le terminal (ordinateur, tablette,
smartphone) utilisé pour générer lattestation. Le code source
de ce générateur d'attestation est ouvert. Il peut être consulté
sur <a href="https://github.com/LAB-MI/attestation-deplacement-derogatoire-q4-2020">
https://github.com/LAB-MI/attestation-deplacement-derogatoire-q4-2020</a>.
</p>
<em>1 - Prénom, Nom, Date de naissance, Lieu de naissance, Adresse,
Code postal, Ville.</em>
<h3 class="titre-3">Traceurs et cookies</h3>
<p>
Lors de la consultation du générateur, des cookies sont déposés
sur le terminal utilisé. Ces cookies sont utilisés pour
sécuriser le service proposé.
</p>
<h4 class="titre-4">Définition dun cookie</h4>
<p>
Un "cookie" est une suite d'informations, généralement de petite
taille et identifié par un nom, qui peut être transmis à votre
navigateur par un site web sur lequel vous vous connectez. Votre
navigateur web le conserve pendant une certaine durée, et le
renvoie au serveur web chaque fois que vous vous y
re-connecterez. Les cookies ont de multiples usages : ils
peuvent servir à mémoriser votre identifiant client auprès d'un
site marchand, le contenu courant de votre panier d'achat, un
identifiant permettant de tracer votre navigation pour des
finalités statistiques ou publicitaires, etc.
</p>
<h4 class="titre-4">Cookies utilisés</h4>
<p>
Différents cookies techniques sont utilisés sur ce générateur
dattestation de déplacement :
</p>
<table class="cookies">
<caption>Liste des cookies utilisés sur ce site/caption>
<thead>
<tr class="header-row">
<th scope="col">Nom du Cookie</th>
<th scope="col">Finalité</th>
<th scope="col">Durée de conservation</th>
</tr>
</thead>
<tbody>
<tr>
<td data-label="Nom du Cookie" class="name-col">incap_ses_*</td>
<td data-label="Finalité" >
Protection DDoS Incapsula et Pare-feu pour application
web : cookie servant à relier les requêtes HTTP à une
session. La réouverture du navigateur et laccès au même
site sont considérés comme des visites différentes. Pour
maintenir les sessions existantes (cookie de session).
</td>
<td data-label="Durée de conservation" >Supprimé après la fermeture du navigateur.</td>
</tr>
<tr>
<td data-label="Nom du Cookie" class="name-col">nlbi_*</td>
<td data-label="Finalité" >
Protection DDoS Incapsula et Pare-feu pour application
web : cookie équilibreur de charge qui garantit que les
requêtes dun client sont envoyées au même serveur
dorigine.
</td>
<td data-label="Durée de conservation" >Supprimé après la fermeture du navigateur.</td>
</tr>
<tr>
<td data-label="Nom du Cookie" class="name-col">visid_incap_*</td>
<td data-label="Finalité" >
Protection DDoS Incapsula et Pare-feu pour application
web : cookie servant à relier certaines sessions à un
visiteur spécifique (visiteur représentant un ordinateur
spécifique). Afin didentifier les clients ayant déjà
consulté Incapsula. Le seul cookie qui perdure pour une
durée de 12 mois.
</td>
<td data-label="Durée de conservation" >12 mois.</td>
</tr>
<tr>
<td data-label="Nom du Cookie" class="name-col">__utm*</td>
<td data-label="Finalité" >
Cookie de classification Incapsula : pour voir comment
le client réagit à et gère un cookie mal formé afin
didentifier la nature de ce client.
</td>
<td data-label="Durée de conservation" >900 secondes maximum.</td>
</tr>
</tbody>
</table>
<p>
Vous pouvez refuser ces cookies en configurant les paramètres de
votre navigateur.
</p>
<h4 class="titre-4">
Moyens d'opposition au dépôt des cookies via votre navigateur
</h4>
<p>
<strong>Si vous utilisez Firefox : </strong>Cliquez sur le
bouton de menu et sélectionnez « Options ». Ensuite,
sélectionnez le panneau « Vie privée et sécurité ». Rendez-vous
à la section « Protection renforcée contre le pistage ».
Choisissez la protection personnalisé [bouton radio
« Personnalisé »]. Décochez la case « cookies ». Cliquez sur le
bouton « Actualisez tous les onglets » si vous souhaitez
appliquer ces changements à la navigation en cours au sein de
votre navigateur.
</p>
<p>
<strong>Si vous utilisez Chrome : </strong>En haut à droite,
cliquez sur « Plus > Paramètres ». En bas, cliquez sur «
Paramètres avancés ». Dans la section "Confidentialité et
sécurité", cliquez sur « Paramètres des sites». Cliquez sur «
Cookies et données des sites». Désactivez loption « Autoriser
les sites à enregistrer/lire les données des cookies ».
</p>
<p>
<strong>Si vous utilisez Internet Explorer : </strong>Sélectionnez le bouton « Outils », puis « Options
Internet ».
Sélectionnez longlet « Confidentialité » puis sous « Paramètres
», sélectionnez « Avancé ». Cochez la case "Ignorer la gestion
automatique des cookies", puis sélectionner "Refuser" dans la
colonne "Cookies tierces parties".
</p>
<p>
Pour plus dinformations sur le sujet, nhésitez pas à visiter
le
<a href="https://www.cnil.fr/fr/cookies-et-autres-traceurs">site de la CNIL</a>.
</p>
</div>
</div>
<div class="btn-wrapper">
<a href="./index.html" class="btn-generateur">
Retour au générateur
</a>
</div>
<footer class="main-footer">
<div class="footer-links">
<a class="footer-line footer-link">Confidentialité</a>
<a href="https://www.interieur.gouv.fr/Infos-du-site/Mentions-legales" target="_blank" title="Mentions légales - nouvelle page"
class="footer-line footer-link">Mentions légales</a>
<a href="https://www.gouvernement.fr/info-coronavirus" target="_blank" title="Information du gouvernement sur le Covid-19 - nouvelle page" class="footer-line footer-link">Informations du
gouvernement sur le Covid-19</a>
<p class="footer-line">
Plus dinfos au<a class="num-08" href="tel:0800130000">
0 800 130 000</a>
</p>
</div>
</footer>
<script src="./js/confidentialite.js"></script>
</body>
</html>

666
src/css/main.css Normal file
View file

@ -0,0 +1,666 @@
@font-face {
font-family: 'marianne-bold';
src: url('../fonts/marianne-bold-webfont.woff2') format('woff2'),
url('../fonts/marianne-bold-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'marianne-regular';
src: url('../fonts/marianne-regular-webfont.woff2') format('woff2'),
url('../fonts/marianne-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
html {
background: white;
}
@supports (font: -apple-system-body) {
html {
font: -apple-system-body !important;
}
}
@media (prefers-color-scheme: dark) {
html {
background: #222;
}
}
body {
margin: 20px;
background: white;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
}
@media (prefers-color-scheme: dark) {
body {
background: #222;
}
}
form, #alert-print {
margin: 30px auto;
max-width: 400px;
}
h1 {
font-size: 2em;
color: black;
}
@media (prefers-color-scheme: dark) {
h1 {
color: white;
background-color: white;
background: #222;
}
}
/* Small devices (landscape phones, 576px and up) */
@media (min-width: 576px) {
h1 {
font-size: 2.5em;
}
}
/* Medium devices (tablets, 768px and up) */
@media (min-width: 768px) {
h1 {
font-size: 3em;
}
}
svg {
height: 1em;
}
h1.flex.flex-wrap {
display: flex;
flex-wrap: wrap;
}
@media (prefers-color-scheme: dark) {
h1.flex.flex-wrap {
border: 1px solid white;
}
}
h2 {
font-size: large;
color: black;
}
@media (prefers-color-scheme: dark) {
h2 {
color: white;
}
}
footer {
margin-top: 40px;
background: white;
}
@media (prefers-color-scheme: dark) {
footer {
background: #222;
}
}
canvas {
border: 1px solid #ced4da;
border-radius: .25rem;
}
a {
color: #000191;
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
a {
color: #A3A3FF;
}
}
p {
color: black;
}
@media (prefers-color-scheme: dark) {
p {
color: white;
}
}
.wrapper {
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
#form-profile .form-radio .form-check {
margin-bottom: 1rem;
}
#form-profile .form-radio-label .form-check-label {
font-weight: 400;
transform: translateY(-2px);
}
@media (prefers-color-scheme: dark) {
#form-profile .form-radio-label .form-check-label {
color: #ddd;
}
}
@media (prefers-color-scheme: dark) {
label {
color: white;
}
.form-control::placeholder {
color: #aaa;
}
input, select, textarea, .form-control, .form-control:focus {
background: #333;
color: white;
border: 1px solid white;
}
.form-control:focus {
background: #555;
}
}
#form-generate .form-radio .form-check {
margin: 10px;
}
#alert-facebook {
position: fixed;
top: 20px;
left: 20px;
right: 20px;
}
#alert-print {
margin: 1rem auto 0;
}
#date-selector-group {
position: relative;
overflow: hidden;
}
#date-selector-group-sortie {
position: relative;
overflow: hidden;
}
#date-selector {
position: absolute;
top: 0;
left: 50%;
height: 100%;
transform: translateX(-50%); /* center the input to avoid reset buttons */
opacity: 0;
z-index: 1;
cursor: pointer;
}
#date-selector-sortie {
position: absolute;
top: 0;
left: 50%;
height: 100%;
transform: translateX(-50%); /* center the input to avoid reset buttons */
opacity: 0;
z-index: 1;
cursor: pointer;
}
input:invalid+span:after {
content: '✖';
padding-left: 0.3em;
}
input:valid+span:after {
content: '✓';
padding-left: 0.3em;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
}
::placeholder {
font-style: italic;
}
.hidden {
display: none !important;
}
.exemple {
margin-top: 0.2em;
font-style: italic;
color: #999999;
height: 1em;
}
.logo {
width: 50%;
}
.covid-title {
display: flex;
align-items: center;
padding: 0.3em;
font-family: 'marianne-regular', Arial, Helvetica, sans-serif;
font-size: 0.50em;
color: #ffffff;
background-color: #000191;
}
@media (prefers-color-scheme: dark) {
.covid-title {
background: #2929ff;
}
}
@media (min-width: 992px) {
.covid-title {
flex-grow: 0;
}
}
.covid-subtitle {
display: inline-flex;
align-items: center;
font-size: 0.40em;
padding: 0.3em;
background-color: #e1000f;
text-transform:uppercase ;
color: #ffffff;
flex-grow: 1;
}
.note-title {
border-bottom: 3px solid #fff;
}
.highlighted {
font-weight: bold;
border-bottom: 2px solid;
}
.footnote-title {
color: #fff;
font-size: 0.9em;
}
.msg-info {
text-align: left;
color: #000000;
}
.msg-alert {
text-align: left;
color: red;
font-style: italic;
}
.text-desc {
font-style :italic;
font-size: 0.8em;
}
@media (prefers-color-scheme: dark) {
.covid-subtitle {
background: #ad000c;
}
}
@media (prefers-color-scheme: dark) {
.msg-info {
color: white;
}
}
.btn-attestation {
padding: 0.9em;
font-size: 1.2em;
font-weight: bold;
color: white;
background-color: #000191;
border-radius: 0.5em;
}
.btn-attestation:hover {
background-color: #3031C1;
}
.github {
font-size: 0.7em;
text-align: center;
color: black;
}
@media (prefers-color-scheme: dark) {
.github {
color: white
}
}
.github-link {
color: #000191;
}
@media (prefers-color-scheme: dark) {
.github-link {
color: #A3A3FF;
}
}
.github-link:hover {
text-decoration: underline;
}
.label-mi {
text-align: center;
font-size: 1em;
color: black;
}
@media (prefers-color-scheme: dark) {
.label-mi {
color: white;
}
}
.validity {
display: inline-flex;
justify-content: center;
width: 2em;
}
@media (prefers-color-scheme: dark) {
.validity {
color: #DDD;
}
}
.main-footer {
display: flex;
justify-content: center;
padding-top: 3em;
padding-bottom: 3em;
background: #222;
color: white;
}
.footer-links {
margin: 0 auto;
max-width: 500px;
background: #222;
}
@media (prefers-color-scheme: dark) {
.main-footer {
background: #444;
}
.footer-links {
background: #444;
}
}
.footer-line {
display: block;
margin: 0.75em;
font-size: 0.9em;
color: #ffffff;
}
.footer-link:hover {
text-decoration: underline;
color: #0a81ff;
}
.footer-link:focus {
color: #ffffff;
}
.num-08 {
font-weight: bold;
color: #00a94f;
}
.num-08:hover {
text-decoration: underline;
color: #0A81FF;
}
.stores-link:hover {
text-decoration: underline;
}
.confidentialite {
.cookies {
border-collapse: collapse;
td,
th {
border: 1px solid #000191;
padding: 8px;
}
@media (prefers-color-scheme: dark) {
td, th {
border-color: #A3A3FF;
color: white;
}
}
.header-row {
font-weight: bold;
color: #ffffff;
background-color: #000191;
text-transform: uppercase;
text-align: center;
}
.name-col {
font-weight: bold;
padding: 8px 16px;
color: #000191;
}
@media (prefers-color-scheme: dark) {
.name-col {
color: #A3A3FF;
}
}
}
.btn-wrapper {
display: flex;
justify-content: center;
.btn-generateur {
padding: 0.8em;
font-size: 1.2em;
font-weight: bold;
color: #ffffff;
background-color: #000191;
border-radius: 0.5em;
text-decoration: none;
color: #ffffff;
}
}
em {
font-size: .8rem;
}
}
.titre-2 {
text-align: left;
font-size: 1.5rem;
font-weight: bold;
color: #000191;
}
.titre-3 {
text-align: left;
font-size: 1.25rem;
font-weight: bold;
color: #000000;
}
.titre-4 {
text-align: left;
font-size: 1rem;
font-weight: bold;
color: #000000;
}
@media (prefers-color-scheme: dark) {
.titre-3 {
color: white;
}
.titre-4 {
color: white;
}
}
@media (prefers-color-scheme: dark) {
.titre-2 {
color: #A3A3FF;
}
}
@media (max-width: 800px){
table thead {
display: none;
}
table tr{
display: block;
margin-bottom: 40px;
}
table td {
display: flex;
text-align: left;
min-height: 3rem;
}
table td:before {
content: attr(data-label);
font-weight: bold;
color: #000191;
width: 100px;
margin-right: 8px;
flex-shrink: 0;
}
}
#snackbar {
min-width: 250px;
color: #fff;
text-align: center;
border-radius: 3px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
font-size: 17px;
transform: translateX(-50%);
box-shadow: 0 0 8px 1px #fff;
opacity: 0;
transition: all 0.5s ease-in-out;
}
#snackbar.show {
opacity: 1;
}
@media only screen and (min-width:600px) {
.hide-on-desktop, * [aria-labelledby='hide-on-desktop'] {
display: none;
max-height: 0;
overflow: hidden;
}
}
.center {
display: block;
margin-left: auto;
margin-right: auto;
}
[id^="footnote"] {
margin: 30px auto;
max-width: 400px;
font-size: 0.8em;
color: #000000;
}
@media (prefers-color-scheme: dark) {
[id^="footnote"] {
color:white;
background: #222;
}
}
#update-alert {
position: fixed;
bottom: 1em;
left: 50%;
transform: translateX(-50%);
}
.alert-info {
color: #030494;
background-color: #caf8ff;
border-color: #caf8ff;
}
.btn-info {
color: #fff;
background-color: #030494;
border-color: #030494;
}
.fieldset {
margin-left: -2em;
margin-right: -2em;
padding-left: 2em;
padding-right: 2em;
border: 3px solid transparent;
}
.legend {
padding-left: 0.5em;
}
.fieldset-error {
border: 3px solid red;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="mstile-150x150.png"/>
<TileColor>#603cba</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/favicons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,38 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="1024.000000pt" height="1024.000000pt" viewBox="0 0 1024.000000 1024.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,1024.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M4858 10236 c-1 -2 -50 -6 -108 -10 -58 -3 -116 -8 -130 -11 -14 -2
-52 -7 -85 -10 -105 -9 -381 -52 -395 -60 -3 -2 -26 -6 -50 -10 -25 -4 -126
-27 -225 -52 -661 -165 -1268 -457 -1830 -879 -82 -62 -161 -123 -175 -136
-14 -13 -55 -48 -90 -78 -243 -205 -529 -511 -739 -790 -39 -52 -75 -100 -80
-106 -22 -27 -153 -228 -218 -334 -178 -293 -340 -637 -453 -965 -69 -202
-140 -458 -174 -635 -2 -14 -9 -47 -15 -75 -13 -62 -20 -106 -55 -360 -37
-258 -43 -912 -11 -1106 2 -13 6 -51 10 -84 3 -33 8 -76 10 -95 11 -75 27
-176 32 -205 3 -16 7 -39 8 -50 7 -43 16 -89 20 -94 2 -4 6 -24 9 -46 6 -38
50 -211 95 -375 93 -339 299 -812 504 -1155 105 -175 101 -169 107 -175 3 -3
27 -39 54 -80 50 -77 254 -351 301 -405 14 -16 49 -57 78 -90 149 -172 466
-486 549 -543 13 -9 32 -25 43 -35 92 -87 397 -307 599 -432 309 -190 707
-377 1035 -485 206 -68 328 -103 400 -117 14 -2 28 -6 31 -8 5 -3 121 -28 231
-50 56 -11 257 -45 299 -50 289 -35 363 -39 675 -39 245 -1 407 5 510 19 17 2
59 7 95 10 36 4 74 9 85 11 11 1 40 6 65 9 81 11 257 44 377 71 65 14 124 27
133 29 8 2 56 15 105 30 50 15 106 31 125 36 56 14 255 83 380 132 260 103
475 207 735 357 160 92 510 340 675 478 218 182 544 511 671 677 13 17 26 32
29 35 14 11 143 183 212 280 66 94 228 346 228 355 0 2 20 37 44 77 45 77 188
360 220 438 11 25 27 63 37 85 125 284 284 828 325 1115 3 19 7 46 9 60 12 62
23 148 31 225 3 33 7 71 10 85 25 158 25 830 -1 1020 -8 63 -16 129 -21 174
-3 27 -6 52 -8 55 -2 3 -7 31 -11 61 -3 30 -8 62 -10 70 -2 8 -7 29 -9 45 -3
17 -10 55 -16 85 -6 30 -14 66 -16 80 -44 226 -187 679 -284 895 -10 22 -32
74 -50 115 -51 118 -200 404 -280 535 -221 366 -482 698 -781 995 -113 113
-258 245 -332 303 -15 12 -39 32 -52 44 -36 32 -280 213 -350 260 -387 258
-753 444 -1160 588 -242 86 -581 177 -780 209 -36 6 -82 14 -110 20 -11 2 -45
7 -75 11 -30 4 -62 9 -71 10 -29 6 -116 15 -294 32 -50 4 -642 12 -647 9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,43 @@
{
"name": "Générateur d'attestation de déplacement dérogatoire",
"short_name": "Déplacement covid-19",
"description": "L'application officielle du gouvernement pour la génération d'attestation de déplacement dérogatoire dématérialisée.",
"categories": ["government", "health"],
"lang": "fr-FR",
"icons": [
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"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",
"theme_color": "#ced4da",
"background_color": "#000191",
"display": "minimal-ui"
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

143
src/form-data.json Normal file
View file

@ -0,0 +1,143 @@
[
[
{
"key": "firstname",
"type": "text",
"contentType": "firstname",
"label": "Prénom",
"autocomplete": "given-name",
"placeholder": "Camille"
},
{
"key": "lastname",
"type": "text",
"contentType": "lastname",
"label": "Nom",
"autocomplete": "family-name",
"placeholder": "Dupont"
},
{
"key": "birthday",
"type": "text",
"contentType": "birthday",
"label": "Date de naissance",
"autocomplete": "birthday",
"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)",
"maxlength": 10,
"placeholder": "01/01/1970"
}
],
[
{
"key": "placeofbirth",
"type": "text",
"contentType": "cityofbirth",
"label": "Lieu de naissance",
"autocomplete": "off",
"placeholder": "Paris"
},
{
"key": "address",
"type": "text",
"contentType": "address",
"label": "Adresse",
"autocomplete": "adress-line1",
"placeholder": "999 avenue de France"
},
{
"key": "city",
"type": "text",
"contentType": "city",
"label": "Ville",
"autocomplete": "address-level2",
"placeholder": "Paris"
},
{
"key": "zipcode",
"type": "number",
"contentType": "zipcode",
"label": "Code Postal",
"autocomplete": "postal-code",
"placeholder": "75001",
"inputmode": "numeric",
"pattern": "[0-9]{5}",
"min": 1000,
"max": 99999,
"minlength":4,
"maxlength":5
}
],
[
{
"key": "creationDate",
"type": "date",
"contentType": "creationDate",
"label": "Date de création",
"isHidden": true
},
{
"key": "creationHour",
"type": "time",
"contentType": "creationHour",
"label": "Heure de création",
"isHidden": true
},
{
"key": "datesortie",
"type": "date",
"contentType": "datesortie",
"label": "Date de sortie",
"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)",
"autocomplete": ""
},
{
"key": "heuresortie",
"type": "time",
"contentType": "heuresortie",
"label": "Heure de sortie",
"autocomplete": ""
},
{
"key": "reason",
"type": "list",
"items": [
{
"code": "travail",
"label": "Déplacements entre le domicile et le lieu d'exercice de l'activité professionnelle ou les déplacements professionnels ne pouvant être différés <a class=\"footnote\" id=\"footnote2\" href=\"#footnote2\">[2]</a>;"
},
{
"code": "achats",
"label": "Déplacements pour effectuer des achats de fournitures nécessaires à l'activité professionnelle, des achats de première nécessité <a class=\"footnote\" id=\"footnote3\" href=\"#footnote3\">[3]</a> dans des établissements dont les activités demeurent autorisées (liste sur gouvernement.fr) et les livraisons à domicile;"
},
{
"code": "sante",
"label": "Consultations et soins ne pouvant être assurés à distance et ne pouvant être différés et lachat de médicaments;"
},
{
"code": "famille",
"label": "Déplacements pour motif familial impérieux, pour l'assistance aux personnes vulnérables et précaires ou la garde d'enfants;"
},
{
"code": "handicap",
"label": "Déplacements des personnes en situation de handicap et de leur accompagnant;"
},
{
"code": "sport_animaux",
"label": "Déplacements brefs, dans la limite d'une heure quotidienne et dans un rayon maximal d'un kilomètre autour du domicile, liés soit à l'activité physique individuelle des personnes, à l'exclusion de toute pratique sportive collective et de toute proximité avec d'autres personnes, soit à la promenade avec les seules personnes regroupées dans un même domicile, soit aux besoins des animaux de compagnie."
},
{
"code": "convocation",
"label": " Convocation judiciaire ou administrative et rendez-vous dans un service public;"
},
{
"code": "missions",
"label": " Participation à des missions d'intérêt général sur demande de l'autorité administrative;"
},
{
"code": "enfants",
"label": "Déplacement pour chercher les enfants à lécole et à loccasion de leurs activités périscolaires;"
}
]
}
]
]

14
src/form.html Normal file
View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Essai génération du formulaire</title>
</head>
<body>
<div class="wrapper">
<form id="form-profile" accept-charset="UTF-8"></form>
</div>
<script src="./js/form.js"></script>
</body>
</html>

12
src/grid.html Normal file
View file

@ -0,0 +1,12 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> </head>
<body>
<label for="file">Sélectionner le pdf</label>
<input type="file" id="file" name="file">
<script src="./js/pdf-grid-helper.js"></script>
</body>
</html>

116
src/index.html Normal file
View file

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="msapplication-TileColor" content="#603cba">
<meta name="msapplication-config" content="./favicons/browserconfig.xml">
<meta name="theme-color" content="#ffffff">
<meta name="title" content="Générateur d'attestation de déplacement dérogatoire - COVID-19">
<meta name="description" content="Ce service officiel génère une version numérique de la déclaration de déplacement covid-19 à présenter aux forces de sécurité lors dun contrôle.">
<meta name="keywords" content="covid19, covid-19, attestation, déclaration, déplacement, officielle, gouvernement">
<meta name="robots" content="index, follow">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="language" content="French">
<meta property="og:title" content="Générateur d'attestation de déplacement dérogatoire - COVID-19" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:description" content="Ce service officiel génère une version numérique de la déclaration de déplacement covid-19 à présenter aux forces de sécurité lors dun contrôle." />
<link rel="canonical" href="https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/" />
<meta property="og:url" content="https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/" />
<meta property="og:site_name" content="Générateur d'attestation de déplacement dérogatoire - COVID-19" />
<script type='application/ld+json'>{"@context":"http://www.schema.org","@type":"GovernmentOrganization","name":"Générateur d'attestation de déplacement dérogatoire - COVID-19","description":"Ce service officiel génère une version numérique de la déclaration de déplacement covid-19 à présenter aux forces de sécurité lors dun contrôle.","address":{"@type":"PostalAddress","addressCountry":"France"}}</script>
<link rel="apple-touch-icon" sizes="180x180" href="./favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="./favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="./favicons/favicon-16x16.png">
<link rel="manifest" href="./favicons/site.webmanifest">
<link rel="mask-icon" href="./favicons/safari-pinned-tab.svg" color="#21bf73">
<title>Attestation de déplacement dérogatoire</title>
</head>
<body>
<header role="banner" class="wrapper">
<picture>
<source srcset="/MIN_Interieur_RVB_dark.svg" media="(prefers-color-scheme: dark)">
<img src="/MIN_Interieur_RVB.svg" alt="Ministère de l'intérieur. Liberté, égalité, fraternité."
class="logo" role="presentation" aria-hidden="true">
</picture>
<div>
<h1 class="flex flex-wrap">
<span class="covid-title">
COVID-19
</span>
<span class="covid-subtitle">
Attestation de déplacement dérogatoire
</span>
</h1>
<p class="text-desc">
En application des mesures générales nécessaires pour faire face à lépidémie de covid-19
dans le cadre de létat durgence sanitaire
</p>
</div>
</header>
<main role="main">
<p
class="alert alert-danger d-none"
role="alert"
id="alert-facebook"
></p>
<div class="wrapper">
<form id="form-profile" accept-charset="UTF-8"></form>
<p class="text-center mt-5">
<button type="button" id="generate-btn" class="btn btn-primary btn-attestation"><span ><i class="fa fa-file-pdf inline-block mr-1"></i> Générer mon attestation</span></button>
</p>
<div class="bg-primary d-none" id="snackbar">
L'attestation est téléchargée sur votre appareil.
</div>
</div>
<div class="">
<p id="footnotes">
<span id="footnote1">
[1] Les personnes souhaitant bénéficier de l'une de ces exceptions doivent se munir s'il y a lieu, lors de leurs déplacements hors de leur domicile, d'un document leur permettant de justifier que le déplacement considéré entre dans le champ de l'une de ces exceptions.
</span><br>
<span id="footnote2">
[2] A utiliser par les travailleurs non salariés, lorsqu'ils ne peuvent disposer d'un justificatif de déplacement établi par leur employeur.
</span><br>
<span id="footnote3">
[3] Y compris les acquisitions à titre gratuit (distribution de denrées alimentaires...) et les déplacements liés à la perception de prestations sociales et au retrait d'espèces.
</span><br>
</p>
<p class="github">
Le code source de ce service est consultable sur <a href="https://github.com/LAB-MI/attestation-deplacement-derogatoire-q4-2020" class="github-link">GitHub</a>.
</p>
<p class="label-mi">
Ministère de l'Intérieur - DNUM - SDIT
</p>
<picture class="center">
<source srcset="/logo_dnum_dark.svg" media="(prefers-color-scheme: dark)">
<img class="center" src="/logo_dnum.svg" alt="logo dnum">
</picture>
</div>
</main>
<footer role="contentinfo" class="main-footer">
<div class="footer-links">
<a href="./confidentialite.html" title="Confidentialité - nouvelle page" target="_blank" class="footer-line footer-link">Confidentialité</a>
<a href="https://www.interieur.gouv.fr/Infos-du-site/Mentions-legales" title="Mentions légales - nouvelle page" target="_blank" class="footer-line footer-link">Mentions légales</a>
<a href="https://www.gouvernement.fr/info-coronavirus" title="Information du gouvernement sur le Covid-19 - nouvelle page" target="_blank" class="footer-line footer-link">Informations du gouvernement sur le Covid-19</a>
<div class="footer-line" >Plus dinfos au <a class="num-08" href="tel:0800130000" title="Numéro vert - appel gratuit depuis un poste fixe en France">0 800 130 000</a></div>
<p class="footer-line" id="version"></p>
</div>
</footer>
<div class="alert alert-info d-none" id="update-alert">
Une nouvelle version est disponible. Cliquer sur le bouton pour l'obtenir.
<p class="text-right">
<button id="reload-btn" class="btn btn-info">Mettre à jour</button>
</p>
</div>
<script src="./js/main.js"></script>
</body>
</html>

35
src/js/check-updates.js Normal file
View file

@ -0,0 +1,35 @@
import { $ } from './dom-utils'
// Ce fichier est généré au build par le plugin parcel-plugin-sw-cache
const swName = '../sw.js'
window.isUpdateAvailable = new Promise(function (resolve, reject) {
// lazy way of disabling service workers while developing
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register(swName)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
installingWorker.onstatechange = () => {
switch (installingWorker.state) {
case 'installed':
if (navigator.serviceWorker.controller) {
// new update available
resolve(true)
} else {
// no update available
resolve(false)
}
break
}
}
}
})
.catch((err) => console.error('[SW ERROR]', err)) // eslint-disable-line no-console
}
})
window.isUpdateAvailable.then((isAvailable) => {
$('#reload-btn').addEventListener('click', () => window.location.reload())
$('#update-alert').classList.remove('d-none')
})

View file

@ -0,0 +1,2 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import '../css/main.css'

43
src/js/dom-utils.js Normal file
View file

@ -0,0 +1,43 @@
export const $ = (...args) => document.querySelector(...args)
export const $$ = (...args) => [...document.querySelectorAll(...args)]
const plainAttributes = [
'for',
'inputmode',
'minlength',
'maxlength',
'min',
'max',
'pattern',
]
export const createElement = (tag, attrs) => {
const el = document.createElement(tag)
plainAttributes.forEach(plainAttr => {
if (attrs && plainAttr in attrs && attrs[plainAttr]) {
el.setAttribute(plainAttr, attrs[plainAttr])
}
if (attrs) {
delete attrs[plainAttr]
}
})
Object.assign(el, attrs)
return el
}
export const appendTo = el => domNodes => {
if (domNodes[Symbol.iterator]) {
el.append(...domNodes)
return
}
el.append(domNodes)
}
export function downloadBlob (blob, fileName) {
const link = createElement('a')
const url = URL.createObjectURL(blob)
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
}

15
src/js/facebook-util.js Normal file
View file

@ -0,0 +1,15 @@
import { $ } from './dom-utils'
export function warnFacebookBrowserUserIfNecessary () {
if (isFacebookBrowser()) {
const alertFacebookElt = $('#alert-facebook')
alertFacebookElt.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."
alertFacebookElt.classList.remove('d-none')
}
}
// see: https://stackoverflow.com/a/32348687/1513045
function isFacebookBrowser () {
const ua = navigator.userAgent || navigator.vendor || window.opera
return ua.includes('FBAN') || ua.includes('FBAV')
}

161
src/js/form-util.js Normal file
View file

@ -0,0 +1,161 @@
import { $, $$, downloadBlob } from './dom-utils'
import { addSlash, getFormattedDate } from './util'
import pdfBase from '../certificate.pdf'
import { generatePdf } from './pdf-util'
const conditions = {
'#field-firstname': {
length: 1,
},
'#field-lastname': {
length: 1,
},
'#field-birthday': {
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-placeofbirth': {
length: 1,
},
'#field-address': {
length: 1,
},
'#field-city': {
length: 1,
},
'#field-zipcode': {
pattern: /\d{5}/g,
},
'#field-datesortie': {
pattern: /\d{4}-\d{2}-\d{2}/g,
},
'#field-heuresortie': {
pattern: /\d{2}:\d{2}/g,
},
}
function validateAriaFields () {
return Object.keys(conditions)
.map((field) => {
const fieldData = conditions[field]
const pattern = fieldData.pattern
const length = fieldData.length
const isInvalidPattern = pattern && !$(field).value.match(pattern)
const isInvalidLength = length && !$(field).value.length
const isInvalid = !!(isInvalidPattern || isInvalidLength)
$(field).setAttribute('aria-invalid', isInvalid)
if (isInvalid) {
$(field).focus()
}
return isInvalid
})
.includes(true)
}
export function setReleaseDateTime (releaseDateInput) {
const loadedDate = new Date()
releaseDateInput.value = getFormattedDate(loadedDate)
}
export function getProfile (formInputs) {
const fields = {}
for (const field of formInputs) {
let value = field.value
if (field.id === 'field-datesortie') {
const dateSortie = field.value.split('-')
value = `${dateSortie[2]}/${dateSortie[1]}/${dateSortie[0]}`
}
fields[field.id.substring('field-'.length)] = value
}
return fields
}
export function getReasons (reasonInputs) {
const reasons = reasonInputs
.filter(input => input.checked)
.map(input => input.value).join(', ')
return reasons
}
export function prepareInputs (formInputs, reasonInputs, reasonFieldset, reasonAlert, snackbar) {
formInputs.forEach((input) => {
const exempleElt = input.parentNode.parentNode.querySelector('.exemple')
const validitySpan = input.parentNode.parentNode.querySelector('.validity')
if (input.placeholder && exempleElt) {
input.addEventListener('input', (event) => {
if (input.value) {
exempleElt.innerHTML = 'ex.&nbsp;: ' + input.placeholder
validitySpan.removeAttribute('hidden')
} else {
exempleElt.innerHTML = ''
}
})
}
})
$('#field-birthday').addEventListener('keyup', function (event) {
event.preventDefault()
const input = event.target
const key = event.keyCode || event.charCode
if (key !== 8 && key !== 46) {
input.value = addSlash(input.value)
}
})
reasonInputs.forEach(radioInput => {
radioInput.addEventListener('change', function (event) {
const isInError = reasonInputs.every(input => !input.checked)
reasonFieldset.classList.toggle('fieldset-error', isInError)
reasonAlert.classList.toggle('hidden', !isInError)
})
})
$('#generate-btn').addEventListener('click', async (event) => {
event.preventDefault()
const reasons = getReasons(reasonInputs)
if (!reasons) {
reasonFieldset.classList.add('fieldset-error')
reasonAlert.classList.remove('hidden')
reasonFieldset.scrollIntoView && reasonFieldset.scrollIntoView()
return
}
const invalid = validateAriaFields()
if (invalid) {
return
}
console.log(getProfile(formInputs), reasons)
const pdfBlob = await generatePdf(getProfile(formInputs), reasons, pdfBase)
const creationInstant = new Date()
const creationDate = creationInstant.toLocaleDateString('fr-CA')
const creationHour = creationInstant
.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)
})
}
export function prepareForm () {
const formInputs = $$('#form-profile input')
const snackbar = $('#snackbar')
const reasonInputs = [...$$('input[name="field-reason"]')]
const reasonFieldset = $('#reason-fieldset')
const reasonAlert = reasonFieldset.querySelector('.msg-alert')
const releaseDateInput = $('#field-datesortie')
setReleaseDateTime(releaseDateInput)
prepareInputs(formInputs, reasonInputs, reasonFieldset, reasonAlert, snackbar)
}

159
src/js/form.js Normal file
View file

@ -0,0 +1,159 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import '../css/main.css'
import formData from '../form-data.json'
import { $, appendTo, createElement } from './dom-utils'
const createTitle = () => {
const h2 = createElement('h2', { className: 'titre-2', innerHTML: 'Remplissez en ligne votre déclaration numérique: ' })
const p = createElement('p', { className: 'msg-info', innerHTML: 'Tous les champs sont obligatoires.' })
return [h2, p]
}
// createElement('div', { className: 'form-group' })
const createFormGroup = ({
autocomplete = false,
autofocus = false,
inputmode,
label,
max,
min,
maxlength,
minlength,
name,
pattern,
placeholder = '',
type = 'text',
}) => {
const formGroup = createElement('div', { className: 'form-group' })
const labelAttrs = {
for: `field-${name}`,
id: `field-${name}-label`,
innerHTML: label,
}
const labelEl = createElement('label', labelAttrs)
const inputGroup = createElement('div', { className: 'input-group align-items-center' })
const inputAttrs = {
autocomplete,
autofocus,
className: 'form-control',
id: `field-${name}`,
inputmode,
min,
max,
minlength,
maxlength,
name,
pattern,
placeholder,
required: true,
type,
}
const input = createElement('input', inputAttrs)
const validityAttrs = {
className: 'validity',
}
const validity = createElement('span', validityAttrs)
const appendToFormGroup = appendTo(formGroup)
appendToFormGroup(labelEl)
appendToFormGroup(inputGroup)
const appendToInputGroup = appendTo(inputGroup)
appendToInputGroup(input)
appendToInputGroup(validity)
return formGroup
}
const createReasonField = (reasonData) => {
const formReasonAttrs = { className: 'form-checkbox align-items-center' }
const formReason = createElement('div', formReasonAttrs)
const appendToReason = appendTo(formReason)
const id = `checkbox-${reasonData.code}`
const inputReasonAttrs = {
className: 'form-check-input',
type: 'checkbox',
id,
name: 'field-reason',
value: reasonData.code,
}
const inputReason = createElement('input', inputReasonAttrs)
const labelAttrs = { innerHTML: reasonData.label, className: 'form-checkbox-label', for: id }
const label = createElement('label', labelAttrs)
appendToReason([inputReason, label])
return formReason
}
const createReasonFieldset = (reasonsData) => {
const fieldsetAttrs = {
id: 'reason-fieldset',
className: 'fieldset',
}
const fieldset = createElement('fieldset', fieldsetAttrs)
const appendToFieldset = appendTo(fieldset)
const legendAttrs = {
className: 'legend titre 3 ',
innerHTML: 'Choisissez un motif de déplacement',
}
const legend = createElement('legend', legendAttrs)
const textAlertAttrs = { className: 'msg-alert hidden', innerHTML: 'Veuillez choisir un motif' }
const textAlert = createElement('p', textAlertAttrs)
const textSubscribeReasonAttrs = {
innerHTML: `certifie que mon déplacement est lié au motif suivant (cocher la case) autorisé en application des
mesures générales nécessaires pour faire face à l'épidémie de Covid19 dans le cadre de l'état
d'urgence sanitaire <a class="footnote" id="footnote1" href="#footnote1">[1]</a>&nbsp;:`,
}
const textSubscribeReason = createElement('p', textSubscribeReasonAttrs)
const reasonsFields = reasonsData.items.map(createReasonField)
appendToFieldset([legend, textAlert, textSubscribeReason, ...reasonsFields])
// Créer un form-checkbox par motif
return fieldset
}
export function createForm () {
const form = $('#form-profile')
// Évite de recréer le formulaire s'il est déjà créé par react-snap (ou un autre outil de prerender)
if (form.innerHTML !== '') {
return
}
const appendToForm = appendTo(form)
const formFirstPart = formData
.flat(1)
.filter(field => field.key !== 'reason')
.filter(field => !field.isHidden)
.map((field,
index) => {
const formGroup = createFormGroup({
autofocus: index === 0,
...field,
name: field.key,
})
return formGroup
})
const reasonsData = formData
.flat(1)
.find(field => field.key === 'reason')
const reasonFieldset = createReasonFieldset(reasonsData)
appendToForm([...createTitle(), ...formFirstPart, reasonFieldset])
}

6
src/js/icons.js Normal file
View file

@ -0,0 +1,6 @@
import { library, dom } from '@fortawesome/fontawesome-svg-core'
import { faFilePdf } from '@fortawesome/free-solid-svg-icons'
library.add(faFilePdf)
dom.watch()

15
src/js/main.js Normal file
View file

@ -0,0 +1,15 @@
import 'bootstrap/dist/css/bootstrap.min.css'
import '../css/main.css'
import './icons'
import './check-updates'
import { prepareForm } from './form-util'
import { warnFacebookBrowserUserIfNecessary } from './facebook-util'
import { addVersion } from './util'
import { createForm } from './form'
warnFacebookBrowserUserIfNecessary()
createForm()
prepareForm()
addVersion(process.env.VERSION)

67
src/js/pdf-grid-helper.js Normal file
View file

@ -0,0 +1,67 @@
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
export async function generatePdf (e) {
e.stopPropagation()
const pdfBase = document.getElementById('file').files[0]
const buffer = await pdfBase.arrayBuffer()
const pdfDoc = await PDFDocument.load(buffer)
const pages = pdfDoc.getPages()
const page1 = pages[0]
const font = await pdfDoc.embedFont(StandardFonts.Helvetica)
const drawText = (text, x, y, size = 11) => {
page1.drawText(text, { x, y, size, font })
}
let x
let y
for (x = 25; x < 1000; x += 25) {
for (y = 25; y < 1000; y += 25) {
drawText('.', {
x: x,
y: y,
size: 11,
font: font,
color: rgb(0.95, 0.1, 0.1),
})
drawText(`${x}`, {
x: x + 3,
y: y,
size: 7,
font: font,
color: rgb(0, 0, 0),
})
drawText(`${y}`, {
x: x + 3,
y: y - 6,
size: 7,
font: font,
color: rgb(0, 0, 0),
})
}
}
pdfDoc.addPage()
const pdfBytes = await pdfDoc.save()
// Trigger the browser to download the PDF document
const pdfAsBlob = new Blob([pdfBytes], { type: 'application/pdf' })
downloadBlob(pdfAsBlob, 'grid.pdf', '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()
}
document.querySelector('#file').addEventListener('change', generatePdf)

140
src/js/pdf-util.js Normal file
View file

@ -0,0 +1,140 @@
import { generateQR } from './util'
import { PDFDocument, StandardFonts } from 'pdf-lib'
const ys = {
travail: 578,
achats: 533,
sante: 477,
famille: 435,
handicap: 396,
sport_animaux: 358,
convocation: 295,
missions: 255,
enfants: 211,
}
export async function generatePdf (profile, reasons, pdfBase) {
const creationInstant = new Date()
const creationDate = creationInstant.toLocaleDateString('fr-FR')
const creationHour = creationInstant
.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
.replace(':', 'h')
const {
lastname,
firstname,
birthday,
placeofbirth,
address,
zipcode,
city,
datesortie,
heuresortie,
} = profile
const data = [
`Cree le: ${creationDate} a ${creationHour}`,
`Nom: ${lastname}`,
`Prenom: ${firstname}`,
`Naissance: ${birthday} a ${placeofbirth}`,
`Adresse: ${address} ${zipcode} ${city}`,
`Sortie: ${datesortie} a ${heuresortie}`,
`Motifs: ${reasons}`,
].join(';\n ')
const existingPdfBytes = await fetch(pdfBase).then((res) => res.arrayBuffer())
const pdfDoc = await PDFDocument.load(existingPdfBytes)
// set pdf metadata
pdfDoc.setTitle('COVID-19 - Déclaration de déplacement')
pdfDoc.setSubject('Attestation de déplacement dérogatoire')
pdfDoc.setKeywords([
'covid19',
'covid-19',
'attestation',
'déclaration',
'déplacement',
'officielle',
'gouvernement',
])
pdfDoc.setProducer('DNUM/SDIT')
pdfDoc.setCreator('')
pdfDoc.setAuthor("Ministère de l'intérieur")
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}`, 119, 696)
drawText(birthday, 119, 674)
drawText(placeofbirth, 297, 674)
drawText(`${address} ${zipcode} ${city}`, 133, 652)
reasons
.split(', ')
.forEach(reason => {
drawText('x', 84, ys[reason], 18)
})
let locationSize = getIdealFontSize(font, profile.city, 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
}
drawText(profile.city, 105, 177, locationSize)
drawText(`${profile.datesortie}`, 91, 153, 11)
drawText(`${profile.heuresortie}`, 264, 153, 11)
// const shortCreationDate = `${creationDate.split('/')[0]}/${
// creationDate.split('/')[1]
// }`
// drawText(shortCreationDate, 314, 189, locationSize)
// // Date création
// drawText('Date de création:', 479, 130, 6)
// drawText(`${creationDate} à ${creationHour}`, 470, 124, 6)
const generatedQR = await generateQR(data)
const qrImage = await pdfDoc.embedPng(generatedQR)
page1.drawImage(qrImage, {
x: page1.getWidth() - 156,
y: 100,
width: 92,
height: 92,
})
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 getIdealFontSize (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
}

35
src/js/util.js Normal file
View file

@ -0,0 +1,35 @@
import QRCode from 'qrcode'
export function generateQR (text) {
const opts = {
errorCorrectionLevel: 'M',
type: 'image/png',
quality: 0.92,
margin: 1,
}
return QRCode.toDataURL(text, opts)
}
export function pad2Zero (str) {
return String(str).padStart(2, '0')
}
export function getFormattedDate (date) {
const year = date.getFullYear()
const month = pad2Zero(date.getMonth() + 1) // Les mois commencent à 0
const day = pad2Zero(date.getDate())
return `${year}-${month}-${day}`
}
export function addSlash (str) {
return str
.replace(/^(\d{2})$/g, '$1/')
.replace(/^(\d{2})\/(\d{2})$/g, '$1/$2/')
.replace(/\/\//g, '/')
}
export function addVersion (version) {
document.getElementById(
'version',
).innerHTML = `${new Date().getFullYear()} - ${version}`
}

1
src/logo_dnum.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

1
src/logo_dnum_dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

1
src/robots.txt Normal file
View file

@ -0,0 +1 @@
Sitemap: https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/sitemap.xml

27
src/sitemap.xml Normal file
View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com -->
<url>
<loc>https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/</loc>
<lastmod>2020-04-06T04:22:03+00:00</lastmod>
<priority>1.00</priority>
</url>
<url>
<loc>https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/confidentialite.html</loc>
<lastmod>2020-04-06T04:22:03+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://media.interieur.gouv.fr/attestation-couvre-feu-covid-19/index.html</loc>
<lastmod>2020-04-06T04:22:03+00:00</lastmod>
<priority>0.64</priority>
</url>
</urlset>