@igea/oac_frontend 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/controllers/users.js +12 -0
- package/src/controllers/vocabolaries.js +22 -0
- package/src/index.js +8 -0
- package/src/locales/en.json +28 -0
- package/src/locales/it.json +27 -1
- package/src/public/css/drop-file.css +19 -0
- package/src/public/js/login/login.js +2 -3
- package/src/public/js/login/password_recovery.js +32 -0
- package/src/public/js/vocabolaries/vocabolaries.js +98 -0
- package/src/views/login.twig +5 -1
- package/src/views/password_recovery.twig +28 -0
- package/src/views/sidebar.twig +8 -0
- package/src/views/users/reset_password.twig +36 -0
- package/src/views/vocabolaries/edit.twig +44 -0
package/package.json
CHANGED
package/src/controllers/users.js
CHANGED
|
@@ -40,6 +40,18 @@ module.exports = function(serviceName) {
|
|
|
40
40
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
router.get('/reset_password/:id/:token', (req, res) => {
|
|
44
|
+
let data = new DataModel(req, {
|
|
45
|
+
root: serviceName,
|
|
46
|
+
title: 'Reset password',
|
|
47
|
+
user: {
|
|
48
|
+
id: req.params.id,
|
|
49
|
+
token: req.params.token
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
res.render('users/reset_password.twig', data.toJson());
|
|
53
|
+
});
|
|
54
|
+
|
|
43
55
|
return router
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const DataModel = require('../models/DataModel');
|
|
4
|
+
|
|
5
|
+
module.exports = function(serviceName) {
|
|
6
|
+
/**
|
|
7
|
+
* @route GET /users/manage: redirect to the users page with list of users
|
|
8
|
+
*/
|
|
9
|
+
router.get('/manage', (req, res) => {
|
|
10
|
+
let data = new DataModel(req, {
|
|
11
|
+
root: serviceName,
|
|
12
|
+
title: 'Vocabolaries Management',
|
|
13
|
+
});
|
|
14
|
+
res.render('vocabolaries/edit.twig', data.toJson());
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return router
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
package/src/index.js
CHANGED
|
@@ -12,6 +12,8 @@ const jwtLib = jwtLibFactory({
|
|
|
12
12
|
secret: process.env.JWT_SECRET || config.jwt_secret,
|
|
13
13
|
excludePaths: [
|
|
14
14
|
`/${serviceName}/`,
|
|
15
|
+
`/${serviceName}/password_recovery`,
|
|
16
|
+
new RegExp(`^/${serviceName}/users/reset_password/[^/]+/[^/]+$`),
|
|
15
17
|
`/${serviceName}/health`,
|
|
16
18
|
`/${serviceName}/captcha/random-image`
|
|
17
19
|
],
|
|
@@ -76,6 +78,9 @@ getPort.default({
|
|
|
76
78
|
const usersRouter = require('./controllers/users.js')(serviceName);
|
|
77
79
|
app.use(`/${serviceName}/users`, usersRouter);
|
|
78
80
|
|
|
81
|
+
const vocabolariesRouter = require('./controllers/vocabolaries.js')(serviceName);
|
|
82
|
+
app.use(`/${serviceName}/vocabolaries`, vocabolariesRouter);
|
|
83
|
+
|
|
79
84
|
const searchRouter = require('./controllers/search.js')(serviceName);
|
|
80
85
|
app.use(`/${serviceName}/search`, searchRouter);
|
|
81
86
|
|
|
@@ -87,6 +92,9 @@ getPort.default({
|
|
|
87
92
|
app.get(`/${serviceName}`, (req, res) => {
|
|
88
93
|
res.render('login', { root: serviceName, title: 'Login' });
|
|
89
94
|
});
|
|
95
|
+
app.get(`/${serviceName}/password_recovery`, (req, res) => {
|
|
96
|
+
res.render('password_recovery', { root: serviceName, title: 'Password recovery' });
|
|
97
|
+
});
|
|
90
98
|
app.get(`/${serviceName}/home`, (req, res) => {
|
|
91
99
|
let data = new DataModel(req, {
|
|
92
100
|
root: serviceName,
|
package/src/locales/en.json
CHANGED
|
@@ -4,9 +4,21 @@
|
|
|
4
4
|
"title": "Login",
|
|
5
5
|
"user": "Email or Username",
|
|
6
6
|
"password": "Password",
|
|
7
|
+
"password_recovery": "Forgot your password?",
|
|
8
|
+
"invalid_user": "Invalid username or password",
|
|
7
9
|
"sign_in": "Sign in",
|
|
8
10
|
"anonymous": "Anonymous"
|
|
9
11
|
},
|
|
12
|
+
"password_recovery": {
|
|
13
|
+
"title": "Password recovery",
|
|
14
|
+
"instructions": "Enter your email address or username to receive password recovery instructions.",
|
|
15
|
+
"email": "Email",
|
|
16
|
+
"send_instructions": "Send instructions",
|
|
17
|
+
"back_to_login": "Back to login",
|
|
18
|
+
"invalid_email": "Invalid email address",
|
|
19
|
+
"email_sent": "Instructions have been sent to your email address, if it exists in our system."
|
|
20
|
+
"password_reset_success": "Your password has been reset successfully. You can now log in with your new password."
|
|
21
|
+
},
|
|
10
22
|
"users": {
|
|
11
23
|
"manage": {
|
|
12
24
|
"title": "Users",
|
|
@@ -44,6 +56,22 @@
|
|
|
44
56
|
"admin": "Admin",
|
|
45
57
|
"editor": "Editor",
|
|
46
58
|
"reader": "Reader"
|
|
59
|
+
},
|
|
60
|
+
"vocabolaries": {
|
|
61
|
+
"title": "Vocabolaries",
|
|
62
|
+
"drag_drop": "Drag & drop XML files here, or click to select",
|
|
63
|
+
"browse": "browse",
|
|
64
|
+
"no_files": "No files selected",
|
|
65
|
+
"file": "file",
|
|
66
|
+
"files": "files",
|
|
67
|
+
"remove": "Remove",
|
|
68
|
+
"upload": "Upload",
|
|
69
|
+
"uploading": "Uploading...",
|
|
70
|
+
"upload_ok": "Files uploaded successfully",
|
|
71
|
+
"upload_error": "Error uploading files",
|
|
72
|
+
"invalid_file": "Invalid file type",
|
|
73
|
+
"max_files": "You can upload up to 5 files at a time",
|
|
74
|
+
"max_size": "Each file must be less than 10MB"
|
|
47
75
|
}
|
|
48
76
|
},
|
|
49
77
|
"search": {
|
package/src/locales/it.json
CHANGED
|
@@ -4,9 +4,21 @@
|
|
|
4
4
|
"title": "Accesso",
|
|
5
5
|
"user": "Email o Nome utente",
|
|
6
6
|
"password": "Password",
|
|
7
|
+
"password_recovery": "Hai dimenticato la password?",
|
|
8
|
+
"invalid_user": "Nome utente o password non validi",
|
|
7
9
|
"sign_in": "Accedi",
|
|
8
10
|
"anonymous": "Anonimo"
|
|
9
11
|
},
|
|
12
|
+
"password_recovery": {
|
|
13
|
+
"title": "Recupero password",
|
|
14
|
+
"instructions": "Inserisci il tuo indirizzo email o nome utente per ricevere le istruzioni di recupero della password.",
|
|
15
|
+
"email": "Email",
|
|
16
|
+
"send_instructions": "Invia istruzioni",
|
|
17
|
+
"back_to_login": "Torna al login",
|
|
18
|
+
"invalid_email": "Indirizzo email non valido",
|
|
19
|
+
"email_sent": "Le istruzioni sono state inviate al tuo indirizzo email, se esiste nel nostro sistema.",
|
|
20
|
+
"password_reset_success": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password."
|
|
21
|
+
},
|
|
10
22
|
"users": {
|
|
11
23
|
"manage": {
|
|
12
24
|
"title": "Utenti",
|
|
@@ -44,7 +56,21 @@
|
|
|
44
56
|
"admin": "Amministratore",
|
|
45
57
|
"editor": "Editore",
|
|
46
58
|
"reader": "Lettore"
|
|
47
|
-
}
|
|
59
|
+
},
|
|
60
|
+
"vocabolaries": {
|
|
61
|
+
"title": "Vocabolari",
|
|
62
|
+
"drag_drop": "Trascina e rilascia i file XML qui, oppure clicca per selezionare",
|
|
63
|
+
"browse": "sfoglia",
|
|
64
|
+
"no_files": "Nessun file selezionato",
|
|
65
|
+
"file": "file",
|
|
66
|
+
"files": "file",
|
|
67
|
+
"remove": "Rimuovi",
|
|
68
|
+
"upload": "Carica",
|
|
69
|
+
"uploading": "Caricamento in corso...",
|
|
70
|
+
"upload_ok": "File caricati con successo",
|
|
71
|
+
"upload_error": "Errore durante il caricamento dei file",
|
|
72
|
+
"invalid_file": "Tipo di file non valido"
|
|
73
|
+
}
|
|
48
74
|
},
|
|
49
75
|
"search": {
|
|
50
76
|
"fast": {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.dropzone {
|
|
2
|
+
width: calc(100% - 200px);
|
|
3
|
+
margin: 40px auto;
|
|
4
|
+
padding: 30px;
|
|
5
|
+
border: 3px dashed darkred;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
text-align: center;
|
|
8
|
+
transition: background .15s, border-color .15s;
|
|
9
|
+
user-select: none;
|
|
10
|
+
}
|
|
11
|
+
.dropzone.dragover {
|
|
12
|
+
background: #f0f8ff;
|
|
13
|
+
border-color: #3b82f6;
|
|
14
|
+
}
|
|
15
|
+
.files-list { margin-top: 12px; text-align: left; }
|
|
16
|
+
.file-row { display:flex; align-items:center; justify-content:space-between; gap: 12px; padding:6px 0; }
|
|
17
|
+
.progress { width: 200px; height: 10px; background:#eee; border-radius:6px; overflow:hidden; }
|
|
18
|
+
.progress > span { display:block; height:100%; width:0%; background:#3b82f6; transition: width .2s; }
|
|
19
|
+
.btn { padding:6px 10px; border-radius:6px; cursor:pointer; border:1px solid #ccc; background:#fff; }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
onSubmit = function(type){
|
|
2
|
-
|
|
3
|
-
axios.post(
|
|
2
|
+
var url = "/backend/auth/authenticate";
|
|
3
|
+
axios.post(url, {
|
|
4
4
|
type: type,
|
|
5
5
|
user: (type == "login") ?
|
|
6
6
|
document.getElementById("username").value : "",
|
|
@@ -11,5 +11,4 @@ onSubmit = function(type){
|
|
|
11
11
|
}).catch(error => {
|
|
12
12
|
console.log(error);
|
|
13
13
|
});
|
|
14
|
-
|
|
15
14
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function passwordrecoveryInit(options={}){
|
|
2
|
+
|
|
3
|
+
window.onSubmit = function(){
|
|
4
|
+
var url = "/backend/auth/password_recovery";
|
|
5
|
+
axios.post(url, {
|
|
6
|
+
user: document.getElementById("username").value
|
|
7
|
+
}).then(response => {
|
|
8
|
+
alert(options.labels.email_sent);
|
|
9
|
+
window.location = "/frontend/";
|
|
10
|
+
}).catch(error => {
|
|
11
|
+
console.log(error);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
window.onSubmitReset = function(){
|
|
16
|
+
|
|
17
|
+
alert(options.labels.reset_success);
|
|
18
|
+
window.location = "/frontend/";
|
|
19
|
+
/*
|
|
20
|
+
var url = "/backend/auth/password_recovery";
|
|
21
|
+
axios.post(url, {
|
|
22
|
+
user: document.getElementById("username").value
|
|
23
|
+
}).then(response => {
|
|
24
|
+
alert(options.labels.email_sent);
|
|
25
|
+
window.location = "/frontend/";
|
|
26
|
+
}).catch(error => {
|
|
27
|
+
console.log(error);
|
|
28
|
+
});
|
|
29
|
+
*/
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
function dropFilesInit(options={}) {
|
|
2
|
+
const dropzone = document.getElementById('dropzone');
|
|
3
|
+
const fileInput = document.getElementById('fileInput');
|
|
4
|
+
const browseBtn = document.getElementById('browseBtn');
|
|
5
|
+
const filesList = document.getElementById('filesList');
|
|
6
|
+
|
|
7
|
+
// Configure your target upload API URL here:
|
|
8
|
+
const UPLOAD_URL = '/backend/fuseki/upload/vocabularies';
|
|
9
|
+
|
|
10
|
+
browseBtn.addEventListener('click', () => fileInput.click());
|
|
11
|
+
fileInput.addEventListener('change', e => handleFiles(e.target.files));
|
|
12
|
+
|
|
13
|
+
// Drag events
|
|
14
|
+
['dragenter','dragover'].forEach(ev => {
|
|
15
|
+
dropzone.addEventListener(ev, (e) => {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
//e.stopPropagation();
|
|
18
|
+
dropzone.classList.add('dragover');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
['dragleave','drop','dragend'].forEach(ev => {
|
|
23
|
+
dropzone.addEventListener(ev, (e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
//e.stopPropagation();
|
|
26
|
+
dropzone.classList.remove('dragover');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
dropzone.addEventListener('drop', (e) => {
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
//e.stopPropagation();
|
|
33
|
+
const dt = e.dataTransfer;
|
|
34
|
+
if (dt && dt.files && dt.files.length) handleFiles(dt.files);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// keyboard accessibility: space or enter triggers browse
|
|
38
|
+
dropzone.addEventListener('keydown', (e) => {
|
|
39
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
fileInput.click();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function handleFiles(fileList) {
|
|
46
|
+
const files = Array.from(fileList);
|
|
47
|
+
files.forEach(uploadFile);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function uploadFile(file) {
|
|
51
|
+
// Create UI row
|
|
52
|
+
const row = document.createElement('div');
|
|
53
|
+
row.className = 'file-row';
|
|
54
|
+
const info = document.createElement('div');
|
|
55
|
+
info.textContent = `${file.name} (${Math.round(file.size / 1024)} KB)`;
|
|
56
|
+
const controls = document.createElement('div');
|
|
57
|
+
|
|
58
|
+
const progressBar = document.createElement('div');
|
|
59
|
+
progressBar.className = 'progress';
|
|
60
|
+
const progressFill = document.createElement('span');
|
|
61
|
+
progressBar.appendChild(progressFill);
|
|
62
|
+
|
|
63
|
+
const status = document.createElement('span');
|
|
64
|
+
status.style.marginLeft = '8px';
|
|
65
|
+
|
|
66
|
+
controls.appendChild(progressBar);
|
|
67
|
+
controls.appendChild(status);
|
|
68
|
+
row.appendChild(info);
|
|
69
|
+
row.appendChild(controls);
|
|
70
|
+
filesList.appendChild(row);
|
|
71
|
+
|
|
72
|
+
// FormData
|
|
73
|
+
var form = new FormData();
|
|
74
|
+
form.append('files', file);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await axios.post(UPLOAD_URL, form, {
|
|
78
|
+
onUploadProgress: (evt) => {
|
|
79
|
+
if (evt.lengthComputable) {
|
|
80
|
+
const pct = Math.round((evt.loaded / evt.total) * 100);
|
|
81
|
+
progressFill.style.width = pct + '%';
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
// Success
|
|
86
|
+
progressFill.style.width = '100%';
|
|
87
|
+
status.textContent = '✔️ ' + options.labels.upload_ok;
|
|
88
|
+
status.style.color = 'green';
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// Error
|
|
91
|
+
status.textContent = '❌ ' + options.labels.upload_error + ': ' + (err.response?.data?.message || err.message);
|
|
92
|
+
status.style.color = 'red';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
package/src/views/login.twig
CHANGED
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
<div>
|
|
15
15
|
<label for="username">{{ t('login.user') }}</label>
|
|
16
16
|
{% if invalidLogin %}
|
|
17
|
-
<span class=".login-message">
|
|
17
|
+
<span class=".login-message">{{ t('login.invalid_user') }}</span>
|
|
18
18
|
{% endif %}
|
|
19
19
|
<input type="text" id="username" name="username" required />
|
|
20
20
|
<label for="password">{{ t('login.password') }}</label>
|
|
21
21
|
<input type="password" id="password" name="password" required />
|
|
22
22
|
<button onclick="onSubmit('login')">{{ t('login.sign_in') }}</button>
|
|
23
|
+
<a href="/{{ root }}/password_recovery">
|
|
24
|
+
<i class="fa-solid fa-key"></i>
|
|
25
|
+
<span>{{ t('login.password_recovery') }}</span>
|
|
26
|
+
</a>
|
|
23
27
|
<button onclick="onSubmit('anonymous');" style="background: #ff0000">
|
|
24
28
|
{{ t('login.anonymous') }}</button>
|
|
25
29
|
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{% set showSidebar = false %}
|
|
2
|
+
|
|
3
|
+
{% extends "base.twig" %}
|
|
4
|
+
|
|
5
|
+
{% block extendHead %}
|
|
6
|
+
<link rel="stylesheet" href="/{{ root }}/css/login.css">
|
|
7
|
+
<script src="/{{ root }}/js/login/password_recovery.js"></script>
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
{% block content %}
|
|
11
|
+
|
|
12
|
+
<div class="login-container">
|
|
13
|
+
<h2><i class="fa-solid fa-key"></i> {{ t('password_recovery.title') }}</h2>
|
|
14
|
+
<h6>{{ t('password_recovery.instructions') }}</h6>
|
|
15
|
+
<div>
|
|
16
|
+
<input type="text" id="username" name="username" required />
|
|
17
|
+
<button onclick="onSubmit()">{{ t('password_recovery.send_instructions') }}</button>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
<script>
|
|
21
|
+
passwordrecoveryInit({
|
|
22
|
+
labels: {
|
|
23
|
+
email_sent: "{{ t('password_recovery.email_sent') }}"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
{% endblock %}
|
package/src/views/sidebar.twig
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
<span>{{ t('users.manage.title') }}</span>
|
|
9
9
|
</a>
|
|
10
10
|
</div>
|
|
11
|
+
|
|
12
|
+
<div class="menu-item">
|
|
13
|
+
<a href="/{{ root }}/vocabolaries/manage">
|
|
14
|
+
<i class="fa-solid fa-book-bookmark"></i>
|
|
15
|
+
<span>{{ t('users.vocabolaries.title') }}</span>
|
|
16
|
+
</a>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
11
19
|
{% endif %}
|
|
12
20
|
|
|
13
21
|
{% if user.id > 0 %}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{% set showSidebar = false %}
|
|
2
|
+
|
|
3
|
+
{% extends "../base.twig" %}
|
|
4
|
+
|
|
5
|
+
{% block extendHead %}
|
|
6
|
+
<link rel="stylesheet" href="/{{ root }}/css/login.css">
|
|
7
|
+
<script src="/{{ root }}/js/login/password_recovery.js"></script>
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
{% block content %}
|
|
11
|
+
|
|
12
|
+
<div class="login-container">
|
|
13
|
+
<h2><i class="fa-solid fa-key"></i> {{ t('password_recovery.title') }}</h2>
|
|
14
|
+
<div>
|
|
15
|
+
|
|
16
|
+
<label for="password">{{ t('users.edit.password') }}</label>
|
|
17
|
+
<input type="password" id="password" name="password" required />
|
|
18
|
+
|
|
19
|
+
<label for="re_password">{{ t('users.edit.confirm_password') }}</label>
|
|
20
|
+
<input type="password" id="re_password" name="re_password" required />
|
|
21
|
+
|
|
22
|
+
<button onclick="onSubmitReset()">{{ t('users.edit.save') }}</button>
|
|
23
|
+
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<script>
|
|
27
|
+
passwordrecoveryInit({
|
|
28
|
+
user: {{ user.id}},
|
|
29
|
+
token: "{{ user.token}}",
|
|
30
|
+
labels: {
|
|
31
|
+
reset_success: "{{ t('password_recovery.password_reset_success') }}"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
{% endblock %}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{% set showSidebar = true %}
|
|
2
|
+
{% extends "../base.twig" %}
|
|
3
|
+
|
|
4
|
+
{% block extendHead %}
|
|
5
|
+
<link rel="stylesheet" href="/{{ root }}/css/drop-file.css">
|
|
6
|
+
<script src="/{{ root }}/js/vocabolaries/vocabolaries.js"></script>
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block content %}
|
|
10
|
+
<div>
|
|
11
|
+
<h2 style="display:flex; justify-content:space-between; align-items:center;">
|
|
12
|
+
<span>{{ t('users.vocabolaries.title') }}:</span>
|
|
13
|
+
</h2>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="dropzone" id="dropzone" draggable="true" tabindex="0">
|
|
17
|
+
<p>{{ t('users.vocabolaries.drag_drop') }}
|
|
18
|
+
<button style="margin-left:20px;" id="browseBtn" class="btn" type="button">
|
|
19
|
+
<i class="fa-solid fa-folder-open"
|
|
20
|
+
style="font-size:24px; color:darkred;">
|
|
21
|
+
</i>
|
|
22
|
+
</button>
|
|
23
|
+
</p>
|
|
24
|
+
<input id="fileInput" type="file" style="display:none" multiple>
|
|
25
|
+
<div class="files-list" id="filesList"></div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
dropFilesInit({
|
|
30
|
+
labels: {
|
|
31
|
+
no_files: "{{ t('users.vocabolaries.no_files') }}",
|
|
32
|
+
remove: "{{ t('users.vocabolaries.remove') }}",
|
|
33
|
+
upload: "{{ t('users.vocabolaries.upload') }}",
|
|
34
|
+
uploading: "{{ t('users.vocabolaries.uploading') }}",
|
|
35
|
+
upload_ok: "{{ t('users.vocabolaries.upload_ok') }}",
|
|
36
|
+
upload_error: "{{ t('users.vocabolaries.upload_error') }}",
|
|
37
|
+
invalid_file: "{{ t('users.vocabolaries.invalid_file') }}",
|
|
38
|
+
max_files: "{{ t('users.vocabolaries.max_files') }}",
|
|
39
|
+
max_size: "{{ t('users.vocabolaries.max_size') }}"
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
{% endblock %}
|