initial commit

This commit is contained in:
2026-08-11 15:14:02 +02:00
parent 29d6c0ce18
commit 15478068a0
26 changed files with 11657 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
+3
View File
@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}
+38
View File
@@ -0,0 +1,38 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="eng">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/alma_logo.png" />
<title>App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
+2559
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.19.0",
"sweetalert2": "^11.26.25",
"vue": "^3.5.40"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.8",
"vite": "^8.1.5",
"vite-plugin-vue-devtools": "^8.1.5"
},
"engines": {
"node": "^22.18.0 || >=24.12.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+366
View File
@@ -0,0 +1,366 @@
<template>
<div class="container-parent">
<div class="files-wrapper">
<div class="container-file-excel">
<FileUploadCard
title="Excel File"
iconClass="fa-file-excel"
iconColor="rgb(56, 160, 80)"
note=".xlsx, .xls"
:disabled="maxFilesReached"
@file-selected="handleFiles"
/>
<p id="result-container" v-if="getExcelFile">
<span class="file-link" @click="previewFile(getExcelFile)">
{{ getExcelFile }}
</span>
<i class="fa-solid fa-circle-xmark" id="circle-mark" @click="openDeleteModal(getExcelFile)"></i>
</p>
<div class="del-file" v-if="deleteFile">
<ModalPopUp
:nameFile="deleteFileName"
@confirm="confirmDelete"
@cancel="closeModal"
/>
</div>
</div>
<div class="container-file-pdf">
<FileUploadCard
title="PDF File"
iconClass="fa-file-pdf"
iconColor="rgb(209, 43, 10)"
note=".pdf"
:disabled="maxFilesReached"
@file-selected="handleFiles"
/>
<p id="result-container" v-if="getPdfFile">
<span class="file-link" @click="previewFile(getPdfFile)">
{{ getPdfFile }}
</span>
<i class="fa-solid fa-circle-xmark" id="circle-mark" @click="openDeleteModal(getPdfFile)"></i>
</p>
<div class="del-file" v-if="deleteFile">
<ModalPopUp
:nameFile="deleteFileName"
@confirm="confirmDelete"
@cancel="closeModal"
/>
</div>
</div>
<p id="alert-message"
v-show="duplicateFileTypes"
>The two files must be of two different types of extension (.xlsx/.xls - .pdf)!</p>
</div>
<div>
<button type="submit" id="upload-btn"
:disabled="!maxFilesReached"
@click="chooseTitleDoc">Next</button>
</div>
<div class="result-http" v-if="resultHTTP">{{ resultHTTP }}</div>
<div class="nameFile" v-if="chooseTitle">
<SelectNameDoc
@selected-value="val => nameDoc = val"
@close-modal="closePopUp"
@proceed="proceedUpload"
:disabled="!nameDoc || !nameDoc.trim()"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import FileUploadCard from './components/FileUploadCard.vue';
import ModalPopUp from './components/ModalPopUp.vue';
import SelectNameDoc from './components/SelectNameDoc.vue';
const allFiles = ref([]);
const deleteFile = ref(false);
const deleteFileName = ref(null);
const duplicateFileTypes = ref(false);
const getExcelFile = ref('');
const getPdfFile = ref('');
const nameDoc = ref('');
const resultHTTP = ref('');
const chooseTitle = ref(false);
const data = ref(null);
const maxFilesReached = computed(() => allFiles.value.length >= 2);
function handleFiles(file) {
if (!file || maxFilesReached.value) return;
const extension = '.' + file.name.split('.').pop().toLowerCase();
const isExcel = extension === '.xlsx' || extension === '.xls';
const isPdf = extension === '.pdf';
// Check if we already have a file of this type loaded
const hasExcel = allFiles.value.some(f => {
const ext = '.' + f.name.split('.').pop().toLowerCase();
return ext === '.xlsx' || ext === '.xls';
});
const hasPdf = allFiles.value.some(f => {
const ext = '.' + f.name.split('.').pop().toLowerCase();
return ext === '.pdf';
});
if ((isExcel && hasExcel) || (isPdf && hasPdf)) {
duplicateFileTypes.value = true;
return;
}
duplicateFileTypes.value = false;
if (isExcel) {
getExcelFile.value = file.name;
} else if (isPdf) {
getPdfFile.value = file.name;
}
allFiles.value.push(file);
}
function previewFile(fileName) {
// Find the actual File object from our array matching the file name
const file = allFiles.value.find(f => f.name === fileName);
if (!file) return;
// Create a temporary URL pointing to the file in browser memory
const fileUrl = URL.createObjectURL(file);
// Open the file in a new browser tab
window.open(fileUrl, '_blank');
}
function openDeleteModal(fileName) {
deleteFile.value = true;
deleteFileName.value = fileName;
}
function confirmDelete() {
if (deleteFileName.value) {
// Remove file from array by name matching
allFiles.value = allFiles.value.filter(f => f.name !== deleteFileName.value);
// Reset reactive file names
if (getExcelFile.value === deleteFileName.value) {
getExcelFile.value = '';
} else if (getPdfFile.value === deleteFileName.value) {
getPdfFile.value = '';
}
}
closeModal();
}
function closeModal() {
deleteFile.value = false;
deleteFileName.value = null;
}
function chooseTitleDoc() {
nameDoc.value = '';
chooseTitle.value = true;
}
function closePopUp() {
nameDoc.value = '';
chooseTitle.value = false;
}
async function proceedUpload() {
if (!nameDoc.value || !nameDoc.value.trim()) {
return;
}
chooseTitle.value = false;
data.value = [...allFiles.value];
const formData = new FormData();
data.value.forEach((file) => {
formData.append('documents', file);
})
formData.append('title', nameDoc.value);
try {
const response = await fetch('/api/documents', {
method: 'POST',
body: formData
});
if (!response.ok) {
console.error(`HTTP Error ${response.status}: ${response.statusText}`);
return;
}
const result = await response.json();
console.log('Server response:', result);
resultHTTP.value = 'File/s uploaded successfully!'
resetForm(); // this resets UI state after successful upload
}
catch (err) {
console.log('Error in procssing the files ',err);
}
}
function resetForm() {
allFiles.value = [];
getExcelFile.value = '';
getPdfFile.value = '';
nameDoc.value = '';
}
</script>
<style>
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
</style>
<style scoped>
.container-parent {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border: none;
box-shadow: 0 0 5px #44986e,
0 0 15px #44986e,
0 0 30px #44986e,
0 0 50px rgba(0, 243, 255, 0.5);
border-radius: 15px;
padding: 20px;
gap: 40px;
height: 92vh;
width: fit-content;
margin: auto;
}
.files-wrapper {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
gap: 60px;
}
.container-file-excel,
.container-file-pdf {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
gap: 30px;
width: 450px;
flex-shrink: 0; /*Tells browser layout engine that under no circumstances should these columns change size when child elements render.*/
flex-grow: 0;
}
#result-container {
position: relative;
border: solid rgb(51, 117, 117) 1px;
border-radius: 10px;
padding: 12px;
padding-right: 35px;
background-color: white;
box-shadow: 4px 4px rgb(51, 117, 117);
color: white;
width: 450px;
color: rgb(51, 117, 117);
width: 100%;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: all 0.1s ease;
}
#result-container:has(#circle-mark:active) {
transform: translateY(2px);
}
.file-link {
display: inline-block; /* Required for transform / translateY to work! */
cursor: pointer;
transition: transform 0.1s ease, color 0.1s ease; /* Move transition to base class for smooth click and release */
}
.file-link:hover {
color: rgb(49, 49, 236);
}
.file-link:active {
color: #193929;
transform: translateY(3px); /* Smooth press animation */
}
#circle-mark {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 14px;
}
.del-file, .nameFile {
/* this covers the entire screen */
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
/* Semi-transparent overlay color */
background-color: rgba(0, 0, 0, 0.4);
/* Blur effect for everything underneath */
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px); /* Safari support */
display: flex;
justify-content: center;
align-items: center;
/* Sit above all other page elements */
z-index: 999;
}
#upload-btn {
margin-top: 30px;
color: white;
background-color: rgb(49, 49, 236);
border: none;
border-radius: 14px;
width: 300px;
padding: 12px;
transition: all 0.1s ease;
cursor: pointer;
}
#upload-btn:hover {
background-color: rgb(60, 116, 238);
}
#upload-btn:disabled {
background-color: rgb(49, 109, 228);
color: white;
cursor: not-allowed;
transform: none; /* Disables active transform effect */
}
#upload-btn:not(:disabled):active {
transform: translateY(2px);
background-color: rgb(12, 12, 158);
}
#alert-message {
position: absolute;
bottom: -50px; /* Positions it nicely below the cards without expanding the box */
left: 50%;
transform: translateX(-50%);
color: red;
font-size: 10px;
}
.result-http {
color: rgb(78, 183, 78);
}
</style>
+1
View File
@@ -0,0 +1 @@
+174
View File
@@ -0,0 +1,174 @@
<template>
<div class="upload-container">
<div class="container">
<div class="title">
<span><i :class="['fa-solid', iconClass]" :style="{ color: iconColor }"></i></span>
<h3>{{ title }}</h3>
</div>
<div
class="container-file"
:class="{ 'is-dragging': isDragging }"
@dragover.prevent="!disabled && (isDragging = true)"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop($event)"
>
<span><i class="fa-solid fa-upload"></i></span>
<p>
Drag and drop excel file here or choose a file
</p>
<p id="note">{{ note }}</p>
<input
type="file"
:id="uniqueID"
:accept="note"
:disabled="disabled"
style="display: none;"
@change="onFileSelected"
/>
<label
:for="disabled ? '' : uniqueID"
id="btn"
:class="{ 'btn-disabled': disabled }"
>Browse file</label>
</div>
</div>
</div>
</template>
<script>
import Swal from 'sweetalert2';
import 'sweetalert2/dist/sweetalert2.min.css';
export default {
props: {
title: { type: String, default: 'Upload File' },
iconClass: { type: String, default: 'fa-file' },
iconColor: { type: String, default: '#000' },
note: { type: String, default: '' },
disabled: { type: Boolean, default: false }
},
data() {
return {
// Generates a unique ID so labels don't collide when multiple cards exist
uniqueID: 'file-upload-' + Math.random().toString(36).substring(2, 9),
isDragging: false
}
},
methods: {
isValidTypeFile(file) {
const fileType = '.' + file.name.split('.').pop().toLowerCase();
const allowedTypes = this.note
.split(',')
.map(type => type.trim().toLowerCase());
return allowedTypes.includes(fileType);
},
onFileSelected(event) {
const file = event.target.files[0]
if (this.isValidTypeFile(file)) {
this.$emit('file-selected', file)
} else {
this.showAlert();
}
event.target.value = '';
},
handleDrop(event) {
if (this.disabled) {
return;
}
this.isDragging = false; // Turn off visual drag highlight
// Get the dropped file from event.dataTransfer
const file = event.dataTransfer.files[0];
if (file && !this.disabled) {
if (this.isValidTypeFile(file)) {
this.$emit('file-selected', file);
}
else {
this.showAlert();
}
}
else {
return;
}
},
showAlert() {
Swal.fire({
title: 'Wrong document type!',
html: `Invalid file type! Only <b>'${ this.note }'</b> files are allowed here.`,
icon: 'info',
confirmButtonText: 'OK',
confirmButtonColor: '#369E7B',
background: '#f8f9fa',
customClass: {
popup: 'custom-swal-popup'
}
});
}
}
}
</script>
<style>
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
* {
font-family: monospace;
}
.title {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.container-file {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 10px;
border: dashed black 2px;
border-radius: 12px;
padding: 30px;
width: 300px;
text-align: center;
}
/* Style highlight when dragging a file over the box */
.container-file.is-dragging {
border-color: rgb(54, 158, 123);
background-color: rgba(54, 158, 123, 0.1);
transform: scale(1.02);
}
#note {
margin-top: 5px;
color: grey;
}
#btn {
color: white;
background-color: rgb(54, 158, 123);
border: none;
border-radius: 10px;
padding: 12px;
transition: all 0.1s ease;
cursor: pointer;
}
#btn:active {
transform: translateY(2px);
background-color: rgb(41, 122, 95);
}
.btn-disabled {
opacity: 0.5;
cursor: not-allowed !important;
transform: none !important;
background-color: rgb(54, 158, 123) !important;
}
.custom-swal-popup {
border-radius: 12px !important;
}
</style>
+81
View File
@@ -0,0 +1,81 @@
<template>
<div class="modal-container">
<div class="title-container">
<p id="title">Remove File</p>
</div>
<hr>
<p id="modal-text" >Are you sure you want to remove "{{ nameFile }}"?</p>
<div class="reply">
<button @click="$emit('confirm')">Yes</button>
<button @click="$emit('cancel')">No</button>
</div>
</div>
</template>
<script>
export default {
props: {
nameFile: {
type: String,
default: 'file_name'
}
}
}
</script>
<style scoped>
* {
color: white;
font-family: monospace;
box-sizing: border-box; /* Ensures padding doesn't expand height beyond 200px */
}
#title {
font-size: 18px;
}
.modal-container {
width: 400px;
height: 200px;
background-color: rgb(32, 59, 50);
border: none;
border-radius: 15px;
padding: 15px;
}
hr {
background-color: white;
border-radius: 10px;
height: 4px;
}
#modal-text {
text-align: center;
margin-top: 15px;
}
.reply {
margin-top: auto;
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
button {
background-color: rgb(37, 110, 85);
border: none;
border-radius: 10px;
padding: 10px 20px;
cursor: pointer;
transition: background-color 0.2s ease;
}
button:hover {
background-color: rgb(58, 171, 133);
}
button:active {
background-color: rgb(54, 158, 123);
transform: translateY(3px);
}
</style>
+151
View File
@@ -0,0 +1,151 @@
<template>
<div class="container">
<span>
<i class="fa-solid
fa-circle-xmark"
id="circle-mark"
@click="$emit('close-modal')"
>
</i>
</span>
<label for="names">Document type:</label><br>
<select name="names"
id="names"
class="styled-select"
required
v-model="selectedDoc"
:disabled="!!customDoc.trim()"
@change="selectedValue"
>
<option value="" disabled selected hidden>Select the pdf doc type...</option>
<option>Carta d'identità</option>
<option>Certificato Lingua</option>
<option>Certificato Medico</option>
<option>CV</option>
<option>Diploma</option>
<option>Documento Medico</option>
<option>Passaporto</option>
<option>Patente</option>
<option>Patto di Corresponsabilità</option>
<option>Permesso di soggiorno</option>
<option>Tampone attestante la negatività a COVID-19</option>
<option>Tesi</option>
<option>Motivational Letter</option>
<option>Stage doc</option>
<option>Dossier di Tirocinio</option>
<option>Docuseal</option>
</select><br><br>
<div class="chooseTitle">If not present, write the doc type here:
<br><br>
<input type="text"
placeholder="Write here..."
id="pdfTitle"
:disabled="!!selectedDoc"
v-model="customDoc"
:class="{ 'customDoc-disabled': !!selectedDoc}"
@input="onInputChange"
>
</div>
<button id="next-btn"
@click="$emit('proceed')"
:disabled="disabled"
:class="{ 'btn-disabled': disabled }"
>Proceed
</button>
</div>
</template>
<script>
export default {
props: {
disabled: { type: Boolean, default: false }
},
data() {
return {
selectedDoc: '',
customDoc: ''
}
},
methods: {
selectedValue() {
this.$emit('selected-value', this.selectedDoc);
},
onInputChange() {
this.$emit('selected-value', this.customDoc);
}
}
}
</script>
<style>
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
</style>
<style scoped>
.container {
position: relative;
display: flex;
flex-direction: column;
text-align: center;
justify-content: center;
align-items: center;
border-radius: 14px;
color: white;
background-color: rgb(32, 59, 50);
padding: 20px;
height: 250px;
}
#circle-mark {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 14px;
}
select.styled-select:invalid {
color: gray;
}
#names, #pdfTitle {
border-radius: 10px;
padding: 10px;
border: none;
box-shadow: 4px 4px 4px black;
outline: none;
}
.chooseTitle > input {
width: 400px;
}
#next-btn {
margin-top: 40px;
border: none;
border-radius: 10px;
padding: 14px;
width: 80px;
background-color: rgb(17, 166, 94);
color: white;
transition: all 0.1s ease;
}
#next-btn:active {
transform: translateY(3px);
background-color: rgb(14, 138, 78);
}
.btn-disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
.customDoc-disabled {
background-color: white;
color: gray;
cursor: not-allowed !important;
}
</style>
+6
View File
@@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
+30
View File
@@ -0,0 +1,30 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
// Base public path
base: '/',
// Dev server proxy setup
server: {
port: 5173,
proxy: {
'/api/documents': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})