Compare commits
3 Commits
product_in
...
feat/new_r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b746e3b6f | ||
|
|
99d623da79 | ||
|
|
a305bd8493 |
4
.env
4
.env
@@ -1,6 +1,6 @@
|
||||
VITE_APP_VERSION="1.2.86"
|
||||
VITE_APP_VERSION="1.2.87"
|
||||
VITE_LANG_DEFAULT="it"
|
||||
VITE_PAO_APP_ID="KKPPAA5KJK435J3KSS9F9D8S9F8SD98F9SDF"
|
||||
VITE_SERVICE_WORKER_FILE="sw-1.2.86.js"
|
||||
VITE_SERVICE_WORKER_FILE="sw-1.2.87.js"
|
||||
VITE_PROJECT_ID_MAIN="5cc0a13fe5c9d156728f400a"
|
||||
VITE_VUE_ROUTER_MODE="history"
|
||||
@@ -10,7 +10,7 @@
|
||||
<meta name="description" content="<%= productDescription %>">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="msapplication-tap-highlight" content="no">
|
||||
<meta name="version" content="1.2.86">
|
||||
<meta name="version" content="1.2.87">
|
||||
<meta name="viewport"
|
||||
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
|
||||
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "riso",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
|
||||
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
|
||||
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -74,6 +74,8 @@ export default defineConfig((ctx) => {
|
||||
'@paths': path.resolve(__dirname, 'src/store/Api/ApiRoutes.ts'),
|
||||
'@images': path.resolve(__dirname, 'src/assets/images'),
|
||||
'@icons': path.resolve(__dirname, 'src/public/myicons'),
|
||||
'@types': path.resolve(__dirname, 'src/types'),
|
||||
'@services': path.resolve(__dirname, 'src/services'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -82,7 +84,7 @@ export default defineConfig((ctx) => {
|
||||
...(viteConf.css || {}),
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: `@use "sass:color"; @use "@/css/variables.scss" as *;`,
|
||||
additionalData: `@use "sass:color"; @use "src/css/variables.scss" as *;`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cnm",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"description": "Comunita Nuovo Mondo",
|
||||
"productName": "ComunitaNuovoMondo",
|
||||
"author": "Surya",
|
||||
@@ -9,7 +9,7 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=8083 APP_VERSION='1.2.86' quasar dev",
|
||||
"dev": "PORT=8083 APP_VERSION='1.2.87' quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production quasar build -m pwa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -25,6 +25,7 @@ const msg_website_it = {
|
||||
Ammetti: 'Ammetti',
|
||||
AbilitaCircuito: 'Abilita Circuito',
|
||||
installaApp: 'Installa App',
|
||||
VideoPage: 'Video',
|
||||
fundraising: 'Sostieni il Progetto',
|
||||
notifs: 'Configura le Notifiche',
|
||||
unsubscribe: 'Disiscriviti',
|
||||
@@ -88,6 +89,7 @@ const msg_website_it = {
|
||||
eventodef: 'Evento:',
|
||||
prova: 'prova',
|
||||
dbop: 'Operazioni',
|
||||
VideoPage: 'Video',
|
||||
dbopmacro: 'Operazioni Macro',
|
||||
projall: 'Comunitari',
|
||||
groups: 'Lista Gruppi',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "freeplanet",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"description": "freeplanet",
|
||||
"productName": "freeplanet",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=8087 APP_VERSION='1.2.86' quasar dev",
|
||||
"dev": "PORT=8087 APP_VERSION='1.2.87' quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8097 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8087 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8097 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8087 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "riso",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
|
||||
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
|
||||
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gruppomacro",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"productName": "Gruppo Macro",
|
||||
"description": "Il Gruppo Editoriale Macro, attivo dal 1987, è leader europeo nella pubblicazione di libri per il benessere e la consapevolezza. Con oltre 1.500 titoli, promuove una visione armonica del mondo, offrendo opere di autori internazionali e italiani come Gregg Braden, Bruce Lipton, Joe Dispenza, Louise Hay, Eckhart Tolle e molti altri. Scopri un'editoria che abbraccia il corpo, la mente, lo spirito e l'ecologia.",
|
||||
"author": "Surya",
|
||||
@@ -9,20 +9,20 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=8089 APP_VERSION='1.2.86' quasar dev",
|
||||
"dev": "PORT=8089 APP_VERSION='1.2.87' quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "APP_VERSION='1.2.86' quasar build -m spa",
|
||||
"buildspa": "APP_VERSION='1.2.87' quasar build -m spa",
|
||||
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8089 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8089 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nuovomondo",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"description": "Nuovo Mondo",
|
||||
"productName": "Nuovo Mondo",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "APP_VERSION='1.2.86' PORT=8083 quasar dev",
|
||||
"dev": "APP_VERSION='1.2.87' PORT=8083 quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8083 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nutriben",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"description": "Nutriben",
|
||||
"productName": "Nutriben",
|
||||
"author": "Surya",
|
||||
@@ -9,20 +9,20 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=8093 APP_VERSION='1.2.86' quasar dev",
|
||||
"dev": "PORT=8093 APP_VERSION='1.2.87' quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "APP_VERSION='1.2.86' quasar build -m spa",
|
||||
"buildspa": "APP_VERSION='1.2.87' quasar build -m spa",
|
||||
"lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\"",
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8099 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8093 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "piuchebuono",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"description": "PiuCheBuono",
|
||||
"productName": "PiuCheBuono",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "PORT=8085 APP_VERSION='1.2.86' quasar dev",
|
||||
"dev": "PORT=8085 APP_VERSION='1.2.87' quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8085 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "riso",
|
||||
"version": "1.2.86",
|
||||
"version": "1.2.87",
|
||||
"productName": "Riso 💚 - Rete Italiana Scambio orizzontale",
|
||||
"description": "Progetto RISO (Rete Italiana Scambio orizzontale) promuove una rete di comunità locali che favoriscono scambi di beni, servizi e ospitalità. Con l'App RISO, sviluppata per facilitare il baratto, il dono e l'uso di monete alternative come i RIS, il progetto crea legami autentici basati sulla fiducia e sostenibilità. Partecipa agli scambi e costruisci una comunità più consapevole e autosufficiente.",
|
||||
"author": "Surya",
|
||||
@@ -9,11 +9,11 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "APP_VERSION='1.2.86' PORT=8084 quasar dev",
|
||||
"dev": "APP_VERSION='1.2.87' PORT=8084 quasar dev",
|
||||
"dev_noCheck": "SKIP_TSC=true quasar dev",
|
||||
"build": "quasar build",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.86' quasar build -m pwa",
|
||||
"buildpwa": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"buildpwatest": "NODE_ENV=production APP_VERSION='1.2.87' quasar build -m pwa",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"type-check:watch": "vue-tsc --noEmit --watch",
|
||||
"buildspa": "quasar build -m spa",
|
||||
@@ -21,8 +21,8 @@
|
||||
"lintfile": "eslint --ext .js,.ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"lintfileNoJS": "eslint --ext .ts,.vue --ignore-path .gitignore ./ > file.out.txt",
|
||||
"fix": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\" --ignore-pattern .gitignore ./ --fix > file.out.txt",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.86' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.86' quasar dev",
|
||||
"pwa": "NODE_ENV=development PORT=8094 APP_VERSION='1.2.87' quasar dev -m pwa",
|
||||
"spa": "NODE_ENV=development PORT=8084 APP_VERSION='1.2.87' quasar dev",
|
||||
"debug": "quasar dev --mode debug",
|
||||
"test": "echo \"No test specified\" && exit 0",
|
||||
"generate-sw": "workbox generateSW workbox-config.js",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/* global workbox */
|
||||
/* global cfgenv */
|
||||
|
||||
const VITE_APP_VERSION = '1.2.86';
|
||||
const VITE_APP_VERSION = '1.2.87';
|
||||
|
||||
// Costanti di configurazione
|
||||
const DYNAMIC_CACHE = 'dynamic-cache-v2';
|
||||
|
||||
@@ -35,6 +35,7 @@ import { colmyUserPeople, colmyUserGroup } from '@store/Modules/fieldsTable';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CFindUsers',
|
||||
emits: ['clickContact'],
|
||||
props: {
|
||||
actionType: {
|
||||
type: Number,
|
||||
@@ -46,6 +47,11 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
enableContactClick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CMyUser,
|
||||
@@ -57,7 +63,7 @@ export default defineComponent({
|
||||
CGridTableRec,
|
||||
CQRCode,
|
||||
},
|
||||
setup(props) {
|
||||
setup(props, { emit }) {
|
||||
const userStore = useUserStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const circuitStore = useCircuitStore();
|
||||
@@ -253,6 +259,10 @@ export default defineComponent({
|
||||
usersList.value.listgroup = receiveRislistgroup;
|
||||
}
|
||||
|
||||
function clickContact(data: any) {
|
||||
emit('clickContact', data)
|
||||
}
|
||||
|
||||
return {
|
||||
userStore,
|
||||
tools,
|
||||
@@ -277,6 +287,7 @@ export default defineComponent({
|
||||
extraparams_groups,
|
||||
filtercustom,
|
||||
arrfilterand,
|
||||
clickContact,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
:showCol="false"
|
||||
:extraparams="extraparams()"
|
||||
:actionType="actionType"
|
||||
@clickContact="clickContact"
|
||||
:enableContactClick="enableContactClick"
|
||||
|
||||
>
|
||||
</CGridTableRec>
|
||||
</div>
|
||||
@@ -99,6 +102,8 @@
|
||||
:extraparams="extraparams_groups()"
|
||||
:actionType="actionType"
|
||||
:visufind="costanti.FIND_GROUP"
|
||||
@clickContact="clickContact"
|
||||
:enableContactClick="enableContactClick"
|
||||
>
|
||||
</CGridTableRec>
|
||||
</div>
|
||||
|
||||
@@ -71,7 +71,7 @@ import { isMap } from 'util/types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CGridTableRec',
|
||||
emits: ['clickButtBar', 'savefilter'],
|
||||
emits: ['clickButtBar', 'savefilter', 'clickContact'],
|
||||
props: {
|
||||
prop_mytitle: {
|
||||
type: String,
|
||||
@@ -377,6 +377,11 @@ export default defineComponent({
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
enableContactClick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CMyPopupEdit,
|
||||
@@ -3230,6 +3235,10 @@ export default defineComponent({
|
||||
return false;
|
||||
}
|
||||
|
||||
function clickContact(data: any) {
|
||||
emit('clickContact', data);
|
||||
}
|
||||
|
||||
created();
|
||||
|
||||
return {
|
||||
@@ -3368,6 +3377,7 @@ export default defineComponent({
|
||||
getisDettagliByCatalog,
|
||||
getisDettagliByRaccolta,
|
||||
hidewindowEdit,
|
||||
clickContact,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -679,6 +679,8 @@
|
||||
col_footer ? tools.getLabelFooterByRow(row, col_footer, tablesel) : ''
|
||||
"
|
||||
@showInnerDialog="showInnerDialog"
|
||||
@clickContact="clickContact"
|
||||
:enableContactClick="enableContactClick"
|
||||
>
|
||||
</CMyUser>
|
||||
|
||||
@@ -701,6 +703,8 @@
|
||||
:finder="false"
|
||||
:mygrp="row"
|
||||
:visu="visufind ? visufind : costanti.FIND_GROUP"
|
||||
@clickContact="clickContact"
|
||||
:enableContactClick="enableContactClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,72 @@
|
||||
// ==========================================
|
||||
// CMENUITEM.SCSS - ORIGINALE + MIGLIORAMENTI RISO
|
||||
// Mantiene tutto il codice originale + stile moderno
|
||||
// CMENUITEM.SCSS
|
||||
// ==========================================
|
||||
|
||||
// ==========================================
|
||||
// VARIABILI COLORI MENU
|
||||
// ==========================================
|
||||
$menu-border-color: #dedede;
|
||||
$menu-border-color-dark: #404040;
|
||||
|
||||
$menu-active-color: #027be3;
|
||||
$menu-active-color-dark: #4da3ff;
|
||||
$menu-active-bg: #dadada;
|
||||
$menu-active-bg-dark: #2a3a4a;
|
||||
|
||||
$menu-hover-bg: rgba(0, 0, 0, 0.04);
|
||||
$menu-hover-bg-dark: rgba(255, 255, 255, 0.08);
|
||||
|
||||
$menu-text-color: rgba(0, 0, 0, 0.87);
|
||||
$menu-text-color-dark: rgba(255, 255, 255, 0.87);
|
||||
|
||||
$menu-icon-color: rgba(0, 0, 0, 0.5);
|
||||
$menu-icon-color-dark: rgba(255, 255, 255, 0.5);
|
||||
|
||||
$subtitle-color: #666;
|
||||
$subtitle-color-dark: #aaa;
|
||||
|
||||
// Colori ruoli
|
||||
$color-admin: #d32f2f;
|
||||
$color-admin-dark: #ef5350;
|
||||
$color-manager: #388e3c;
|
||||
$color-manager-dark: #66bb6a;
|
||||
$color-socio: #1b5e20;
|
||||
$color-socio-dark: #4caf50;
|
||||
$color-facilitatore: #4a148c;
|
||||
$color-facilitatore-dark: #9c27b0;
|
||||
$color-collaboratore: #f57c00;
|
||||
$color-collaboratore-dark: #ffb74d;
|
||||
$color-editor: #6a1b9a;
|
||||
$color-editor-dark: #ab47bc;
|
||||
$color-commerciale: #e65100;
|
||||
$color-commerciale-dark: #ff9800;
|
||||
$color-grafico: #00796b;
|
||||
$color-grafico-dark: #26a69a;
|
||||
$color-doc: #42a5f5;
|
||||
$color-doc-dark: #64b5f6;
|
||||
|
||||
// ==========================================
|
||||
// DEBUG CLASS
|
||||
// ==========================================
|
||||
.prova {
|
||||
color: red;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// BASE MENU STYLES
|
||||
// ==========================================
|
||||
.q-list-header {
|
||||
min-height: 12px;
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.menu-hr {
|
||||
border-color: #dedede;
|
||||
border-color: $menu-border-color;
|
||||
height: 0.5px;
|
||||
|
||||
.body--dark & {
|
||||
border-color: $menu-border-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.list-label:first-child {
|
||||
@@ -23,90 +75,266 @@
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ROUTER LINK ACTIVE
|
||||
// ==========================================
|
||||
.router-link-active {
|
||||
color: #027be3;
|
||||
background-color: #dadada !important;
|
||||
border-right: 2px solid #027be3;
|
||||
}
|
||||
|
||||
.router-link-active .item-primary {
|
||||
color: #027be3;
|
||||
color: $menu-active-color;
|
||||
background-color: $menu-active-bg !important;
|
||||
border-right: 2px solid $menu-active-color;
|
||||
|
||||
.body--dark & {
|
||||
color: $menu-active-color-dark;
|
||||
background-color: $menu-active-bg-dark !important;
|
||||
border-right-color: $menu-active-color-dark;
|
||||
}
|
||||
|
||||
.item-primary {
|
||||
color: $menu-active-color;
|
||||
|
||||
.body--dark & {
|
||||
color: $menu-active-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MENU ARROW
|
||||
// ==========================================
|
||||
.menu_freccina {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: inline-block;
|
||||
padding: 0 0 0 0;
|
||||
-webkit-transform: rotate(-180deg);
|
||||
padding: 0;
|
||||
transform: rotate(-180deg);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MENU SIZES
|
||||
// ==========================================
|
||||
.my-menu,
|
||||
.my-menu>i {
|
||||
.my-menu > i {
|
||||
min-height: 40px;
|
||||
min-width: 26px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.my-menu-small,
|
||||
.my-menu-small>i {
|
||||
.my-menu-small > i {
|
||||
min-height: 40px;
|
||||
min-width: 26px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.isAdmin {
|
||||
color: red !important;
|
||||
}
|
||||
|
||||
.isSocioResidente {
|
||||
color: darkgreen;
|
||||
}
|
||||
|
||||
.isCalendar {}
|
||||
|
||||
.isManager {
|
||||
color: green !important;
|
||||
}
|
||||
|
||||
.isFacilitatore {
|
||||
color: #201a80;
|
||||
}
|
||||
|
||||
.onlyCollaboratore {
|
||||
color: #bd7b10;
|
||||
}
|
||||
|
||||
.my-menu-icon {
|
||||
min-width: 2px;
|
||||
font-size: 1rem;
|
||||
|
||||
> i {
|
||||
min-width: 26px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.my-menu-icon>i {
|
||||
min-width: 26px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.clexpansion {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.my-menu-active {
|
||||
background-color: rgba(174, 189, 241, 0.71);
|
||||
}
|
||||
|
||||
.my-menu-separat>i {
|
||||
.my-menu-separat > i {
|
||||
min-width: 26px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.my-menu-icon-none>i {
|
||||
.my-menu-icon-none > i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.clicon img,
|
||||
.clicon {
|
||||
// ==========================================
|
||||
// ROLE COLORS - Con Dark Mode
|
||||
// ==========================================
|
||||
.isAdmin {
|
||||
color: $color-admin !important;
|
||||
border-left: 3px solid $color-admin;
|
||||
background: linear-gradient(90deg, rgba($color-admin, 0.08) 0%, rgba($color-admin, 0.02) 100%);
|
||||
|
||||
.body--dark & {
|
||||
color: $color-admin-dark !important;
|
||||
border-left-color: $color-admin-dark;
|
||||
background: linear-gradient(90deg, rgba($color-admin-dark, 0.15) 0%, rgba($color-admin-dark, 0.05) 100%);
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
background: rgba($color-admin, 0.12) !important;
|
||||
|
||||
.q-icon {
|
||||
color: $color-admin;
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
background: rgba($color-admin-dark, 0.2) !important;
|
||||
|
||||
.q-icon {
|
||||
color: $color-admin-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, rgba($color-admin, 0.12) 0%, rgba($color-admin, 0.04) 100%);
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(90deg, rgba($color-admin-dark, 0.2) 0%, rgba($color-admin-dark, 0.08) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isManager {
|
||||
color: $color-manager !important;
|
||||
border-left: 3px solid $color-manager;
|
||||
background: linear-gradient(90deg, rgba($color-manager, 0.08) 0%, rgba($color-manager, 0.02) 100%);
|
||||
|
||||
.body--dark & {
|
||||
color: $color-manager-dark !important;
|
||||
border-left-color: $color-manager-dark;
|
||||
background: linear-gradient(90deg, rgba($color-manager-dark, 0.15) 0%, rgba($color-manager-dark, 0.05) 100%);
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
background: rgba($color-manager, 0.12) !important;
|
||||
|
||||
.q-icon {
|
||||
color: $color-manager;
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
background: rgba($color-manager-dark, 0.2) !important;
|
||||
|
||||
.q-icon {
|
||||
color: $color-manager-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, rgba($color-manager, 0.12) 0%, rgba($color-manager, 0.04) 100%);
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(90deg, rgba($color-manager-dark, 0.2) 0%, rgba($color-manager-dark, 0.08) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isSocioResidente {
|
||||
color: $color-socio !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-socio-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-socio;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-socio-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isFacilitatore {
|
||||
color: $color-facilitatore !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-facilitatore-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-facilitatore;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-facilitatore-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.onlyCollaboratore {
|
||||
color: $color-collaboratore !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-collaboratore-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-collaboratore;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-collaboratore-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isEditor {
|
||||
color: $color-editor !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-editor-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-editor;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-editor-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isCommerciale {
|
||||
color: $color-commerciale !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-commerciale-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-commerciale;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-commerciale-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isGrafico {
|
||||
color: $color-grafico !important;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-grafico-dark !important;
|
||||
}
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: $color-grafico;
|
||||
|
||||
.body--dark & {
|
||||
color: $color-grafico-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isDoc {
|
||||
border-left: 3px solid $color-doc;
|
||||
background: linear-gradient(90deg, rgba($color-doc, 0.08) 0%, rgba($color-doc, 0.02) 100%);
|
||||
|
||||
.body--dark & {
|
||||
border-left-color: $color-doc-dark;
|
||||
background: linear-gradient(90deg, rgba($color-doc-dark, 0.15) 0%, rgba($color-doc-dark, 0.05) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.isCalendar {
|
||||
// Placeholder per eventuali stili calendario
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ICONS & AVATARS
|
||||
// ==========================================
|
||||
.clicon,
|
||||
.clicon img {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -114,100 +342,151 @@
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.OLD_q-item__section--side {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.imgicon img {
|
||||
font-size: 2.5rem !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/*
|
||||
.menu-enter-active, .scale-enter {
|
||||
-webkit-animation: moveFromTopFade .5s ease both;
|
||||
animation: moveFromTopFade .5s ease both;
|
||||
.clexpansion {
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
.menu-leave-to, .scale-leave-active {
|
||||
-webkit-animation: moveToBottom .5s ease both;
|
||||
animation: moveToBottom .5s ease both;
|
||||
}
|
||||
*/
|
||||
// ==========================================
|
||||
// MENU ACTIVE STATE
|
||||
// ==========================================
|
||||
.my-menu-active {
|
||||
background: linear-gradient(90deg, rgba($menu-active-color, 0.12) 0%, rgba($menu-active-color, 0.05) 100%) !important;
|
||||
border-radius: 8px;
|
||||
border-right: 3px solid $menu-active-color;
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(90deg, rgba($menu-active-color-dark, 0.2) 0%, rgba($menu-active-color-dark, 0.08) 100%) !important;
|
||||
border-right-color: $menu-active-color-dark;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $menu-active-color;
|
||||
font-weight: 600;
|
||||
|
||||
.body--dark & {
|
||||
color: $menu-active-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
background: rgba($menu-active-color, 0.1) !important;
|
||||
|
||||
.body--dark & {
|
||||
background: rgba($menu-active-color-dark, 0.2) !important;
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
color: $menu-active-color;
|
||||
|
||||
.body--dark & {
|
||||
color: $menu-active-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg, rgba($menu-active-color, 0.16) 0%, rgba($menu-active-color, 0.08) 100%) !important;
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(90deg, rgba($menu-active-color-dark, 0.25) 0%, rgba($menu-active-color-dark, 0.12) 100%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// BIG MENU - Titoli grandi
|
||||
// ==========================================
|
||||
.bigmenu {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0.0512rem 0.052rem .01rem #555;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: -0.01em;
|
||||
|
||||
.body--dark & {
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SUBTITLE
|
||||
// ==========================================
|
||||
.subtitle {
|
||||
font-size: 0.85em;
|
||||
color: #666;
|
||||
font-size: 0.8rem;
|
||||
color: $subtitle-color;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
opacity: 0.85;
|
||||
line-height: 1.3;
|
||||
|
||||
.body--dark & {
|
||||
color: $subtitle-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MIGLIORAMENTI STILE RISO - AGGIUNTI
|
||||
// QUASAR COMPONENT OVERRIDES
|
||||
// ==========================================
|
||||
|
||||
// ==========================================
|
||||
// INDENTAZIONE LIVELLI - Più compatta
|
||||
// ==========================================
|
||||
[style*="paddingLeft"] {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Q-SEPARATOR - Divisore migliorato
|
||||
// ==========================================
|
||||
// Q-SEPARATOR
|
||||
.q-separator {
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(0, 0, 0, 0.1) 50%,
|
||||
transparent 100%);
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.1) 50%, transparent 100%);
|
||||
margin: 6px 0;
|
||||
height: 1px;
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Q-EXPANSION-ITEM - Espandibile migliorato
|
||||
// ==========================================
|
||||
// Q-EXPANSION-ITEM
|
||||
.q-expansion-item {
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
// Header dell'expansion
|
||||
|
||||
> .q-item {
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
background: $menu-hover-bg;
|
||||
transform: translateX(2px);
|
||||
|
||||
.body--dark & {
|
||||
background: $menu-hover-bg-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label header
|
||||
|
||||
.q-item__label {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// Icona expand/collapse
|
||||
|
||||
.q-icon {
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
color: $menu-icon-color;
|
||||
|
||||
// Quando espanso
|
||||
.q-expansion-item--expanded {
|
||||
> .q-item {
|
||||
.body--dark & {
|
||||
color: $menu-icon-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
// Expanded state
|
||||
&--expanded > .q-item {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
|
||||
|
||||
.body--dark & {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
color: var(--q-primary);
|
||||
transform: rotate(0deg);
|
||||
@@ -215,52 +494,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Q-ITEM FOGLIA - Voce menu semplice
|
||||
// ==========================================
|
||||
// Q-ITEM
|
||||
.q-item {
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
min-height: 42px;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
background: $menu-hover-bg;
|
||||
transform: translateX(3px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
|
||||
.body--dark & {
|
||||
background: $menu-hover-bg-dark;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:active {
|
||||
transform: translateX(1px);
|
||||
}
|
||||
|
||||
// Sezione thumbnail con avatar
|
||||
|
||||
.q-item__section--thumbnail {
|
||||
min-width: 36px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
// Label principale
|
||||
|
||||
> .q-item__section > span:not(.subtitle) {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
color: $menu-text-color;
|
||||
|
||||
.body--dark & {
|
||||
color: $menu-text-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Q-AVATAR - Icone migliorate
|
||||
// ==========================================
|
||||
// Q-AVATAR
|
||||
.q-avatar {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: transparent !important;
|
||||
|
||||
|
||||
&.rounded {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.q-icon {
|
||||
font-size: 1.3rem;
|
||||
transition: all 0.3s ease;
|
||||
@@ -269,212 +551,28 @@
|
||||
|
||||
.q-item:hover .q-avatar {
|
||||
transform: scale(1.08);
|
||||
|
||||
|
||||
.q-icon {
|
||||
transform: rotate(-5deg);
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MY-MENU-ACTIVE - Stato attivo
|
||||
// INDENTATION TRANSITION
|
||||
// ==========================================
|
||||
.my-menu-active {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(2, 123, 227, 0.12) 0%,
|
||||
rgba(2, 123, 227, 0.05) 100%) !important;
|
||||
border-radius: 8px;
|
||||
border-right: 3px solid #027be3;
|
||||
|
||||
span {
|
||||
color: #027be3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
background: rgba(2, 123, 227, 0.1) !important;
|
||||
|
||||
.q-icon {
|
||||
color: #027be3;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(2, 123, 227, 0.16) 0%,
|
||||
rgba(2, 123, 227, 0.08) 100%) !important;
|
||||
}
|
||||
[style*="paddingLeft"] {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CLASSI PERMESSI - Badge colorati
|
||||
// ==========================================
|
||||
.isAdmin {
|
||||
color: #d32f2f !important;
|
||||
border-left: 3px solid #d32f2f;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(211, 47, 47, 0.08) 0%,
|
||||
rgba(211, 47, 47, 0.02) 100%);
|
||||
|
||||
.q-avatar {
|
||||
background: rgba(211, 47, 47, 0.12) !important;
|
||||
|
||||
.q-icon {
|
||||
color: #d32f2f;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(211, 47, 47, 0.12) 0%,
|
||||
rgba(211, 47, 47, 0.04) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.isManager {
|
||||
color: #388e3c !important;
|
||||
border-left: 3px solid #388e3c;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(56, 142, 60, 0.08) 0%,
|
||||
rgba(56, 142, 60, 0.02) 100%);
|
||||
|
||||
.q-avatar {
|
||||
background: rgba(56, 142, 60, 0.12) !important;
|
||||
|
||||
.q-icon {
|
||||
color: #388e3c;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg,
|
||||
rgba(56, 142, 60, 0.12) 0%,
|
||||
rgba(56, 142, 60, 0.04) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.isSocioResidente {
|
||||
color: #1b5e20 !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #1b5e20;
|
||||
}
|
||||
}
|
||||
|
||||
.isFacilitatore {
|
||||
color: #4a148c !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #4a148c;
|
||||
}
|
||||
}
|
||||
|
||||
.onlyCollaboratore {
|
||||
color: #f57c00 !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #f57c00;
|
||||
}
|
||||
}
|
||||
|
||||
.isEditor {
|
||||
color: #6a1b9a !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #6a1b9a;
|
||||
}
|
||||
}
|
||||
|
||||
.isCommerciale {
|
||||
color: #e65100 !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #e65100;
|
||||
}
|
||||
}
|
||||
|
||||
.isGrafico {
|
||||
color: #00796b !important;
|
||||
|
||||
.q-avatar .q-icon {
|
||||
color: #00796b;
|
||||
}
|
||||
}
|
||||
|
||||
.isDoc {
|
||||
border-left: 3px solid #42a5f5;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(66, 165, 245, 0.08) 0%,
|
||||
rgba(66, 165, 245, 0.02) 100%);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SUBTITLE - Testo secondario
|
||||
// ==========================================
|
||||
.subtitle {
|
||||
font-size: 0.8rem;
|
||||
color: #757575;
|
||||
font-style: italic;
|
||||
margin-top: 2px;
|
||||
opacity: 0.85;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// BIGMENU - Titoli grandi
|
||||
// ==========================================
|
||||
.bigmenu {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// CLASSI EXTRA - Supporto customizzazioni
|
||||
// EXTRA CLASSES
|
||||
// ==========================================
|
||||
.extraclass {
|
||||
// Placeholder per classi custom
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RESPONSIVE
|
||||
// ==========================================
|
||||
@media screen and (max-width: 600px) {
|
||||
.q-item {
|
||||
padding: 5px 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.q-expansion-item > .q-item {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
width: 1.8rem !important;
|
||||
height: 1.8rem !important;
|
||||
font-size: 1.8rem !important;
|
||||
|
||||
.q-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.q-item__section--thumbnail {
|
||||
min-width: 32px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.bigmenu {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ANIMAZIONI ENTRATA
|
||||
// ANIMATIONS
|
||||
// ==========================================
|
||||
@keyframes fadeSlideIn {
|
||||
from {
|
||||
@@ -493,11 +591,50 @@
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// FOCUS ACCESSIBILITA
|
||||
// ACCESSIBILITY - Focus States
|
||||
// ==========================================
|
||||
.q-item:focus-visible,
|
||||
.q-expansion-item:focus-visible {
|
||||
outline: 2px solid var(--q-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 8px;
|
||||
.q-item,
|
||||
.q-expansion-item {
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--q-primary);
|
||||
outline-offset: 2px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// RESPONSIVE
|
||||
// ==========================================
|
||||
@media screen and (max-width: 600px) {
|
||||
.q-item {
|
||||
padding: 5px 8px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.q-expansion-item > .q-item {
|
||||
padding: 5px 8px;
|
||||
}
|
||||
|
||||
.q-avatar {
|
||||
width: 1.8rem !important;
|
||||
height: 1.8rem !important;
|
||||
font-size: 1.8rem !important;
|
||||
|
||||
.q-icon {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.q-item__section--thumbnail {
|
||||
min-width: 32px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.bigmenu {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { useCircuitStore } from '@store/CircuitStore'
|
||||
export default defineComponent({
|
||||
name: 'CMyUser',
|
||||
components: { CSendCoins, CSaldo, CUserInfoAccount },
|
||||
emits: ['setCmd', 'showInnerDialog'],
|
||||
emits: ['setCmd', 'showInnerDialog', 'clickContact'],
|
||||
props: {
|
||||
mycontact: {
|
||||
type: Object as PropType<IUserFields | null>,
|
||||
@@ -72,6 +72,11 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
enableContactClick: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -128,13 +133,17 @@ export default defineComponent({
|
||||
emit('showInnerDialog', showsendCoinTo.value)
|
||||
}
|
||||
|
||||
function clickToUser(username: string) {
|
||||
if (props.actionType === costanti.ACTIONTYPE.SEND_RIS)
|
||||
naviga(`/my/` + username + '?sr=0')
|
||||
else if (props.actionType === costanti.ACTIONTYPE.LINK_REG)
|
||||
naviga(`/registrati/` + username)
|
||||
else
|
||||
naviga(`/my/` + username)
|
||||
function clickToUser(username: string, recuser: any) {
|
||||
if (props.enableContactClick) {
|
||||
emit('clickContact', { username, recuser })
|
||||
} else {
|
||||
if (props.actionType === costanti.ACTIONTYPE.SEND_RIS)
|
||||
naviga(`/my/` + username + '?sr=0')
|
||||
else if (props.actionType === costanti.ACTIONTYPE.LINK_REG)
|
||||
naviga(`/registrati/` + username)
|
||||
else
|
||||
naviga(`/my/` + username)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(mounted)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
>
|
||||
<q-item-section
|
||||
avatar
|
||||
@click="clickToUser(contact.username)"
|
||||
@click="clickToUser(contact.username, contact)"
|
||||
>
|
||||
<q-avatar size="60px">
|
||||
<q-img
|
||||
@@ -18,7 +18,7 @@
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section @click="clickToUser(contact.username)">
|
||||
<q-item-section @click="clickToUser(contact.username, contact)">
|
||||
<q-item-label v-if="labelextra && labelextra !== contact.username"
|
||||
><strong>{{ labelextra }}</strong></q-item-label
|
||||
>
|
||||
|
||||
@@ -362,9 +362,7 @@ export default defineComponent({
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (load) {
|
||||
fine_caricamento();
|
||||
}
|
||||
fine_caricamento();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ref, reactive } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import type { InvitoAmicoForm, InvitoAmicoResponse } from './invita-amico.types';
|
||||
import type { InvitoAmicoForm, InvitoAmicoResponse } from '../../types/invita-amico.types';
|
||||
|
||||
// Composables
|
||||
const $q = useQuasar();
|
||||
|
||||
@@ -425,89 +425,131 @@ display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- Dialog annunci ---------------- */
|
||||
|
||||
.annunci-dialog {
|
||||
width: min(560px, 92vw);
|
||||
border-radius: $r-lg;
|
||||
overflow: hidden;
|
||||
}
|
||||
max-width: 500px;
|
||||
width: 90vw;
|
||||
|
||||
.dialog-header {
|
||||
padding: $s-md;
|
||||
|
||||
.dialog-title-row {
|
||||
.dialog-header {
|
||||
background: $gradient-primary;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $s-lg;
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: $s-md;
|
||||
}
|
||||
|
||||
.annunci-options-mobile {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $s-sm;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
margin: 0;
|
||||
font-weight: 900;
|
||||
font-size: 1.15rem;
|
||||
.annuncio-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $s-sm;
|
||||
padding: $s-md;
|
||||
border-radius: $r-lg;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 140px;
|
||||
justify-content: center;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-2px) scale(1);
|
||||
}
|
||||
|
||||
.q-icon {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.option-subtitle {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&.gradient-indigo {
|
||||
background: $gradient-indigo;
|
||||
}
|
||||
|
||||
&.gradient-red {
|
||||
background: $gradient-red;
|
||||
}
|
||||
|
||||
&.gradient-lime {
|
||||
background: $gradient-lime;
|
||||
}
|
||||
|
||||
&.gradient-teal {
|
||||
background: $gradient-teal;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.72;
|
||||
@media (max-width: 599px) {
|
||||
.annuncio-option {
|
||||
padding: $s-md;
|
||||
min-height: 130px;
|
||||
|
||||
.q-icon {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.option-subtitle {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.annunci-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.annunci-item {
|
||||
min-height: 74px;
|
||||
color: white;
|
||||
border-radius: 0;
|
||||
transition: filter 140ms ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* Quasar adds a light overlay on hover/focus; we force a darker overlay so white titles stay readable */
|
||||
.q-focus-helper {
|
||||
background: rgba(0, 0, 0, 0.22) !important;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
&:hover .q-focus-helper,
|
||||
&:focus .q-focus-helper,
|
||||
&.q-item--active .q-focus-helper,
|
||||
&.q-item--highlighted .q-focus-helper {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: saturate(1.06);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.annunci-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
border: 1px solid rgba(255, 255, 255, 0.20);
|
||||
}
|
||||
|
||||
.option-title {
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.option-subtitle {
|
||||
opacity: 0.92;
|
||||
}
|
||||
}
|
||||
|
||||
/* gradients used in list */
|
||||
.gradient-green { background: linear-gradient(135deg, #22c55e, #16a34a); }
|
||||
|
||||
@@ -136,14 +136,6 @@ export default defineComponent({
|
||||
// TODO: aprire dialog/pagina ricezione RIS
|
||||
};
|
||||
|
||||
const inviteFriend = () => {
|
||||
// TODO: aprire dialog/pagina invito amico
|
||||
};
|
||||
|
||||
const showMembers = () => {
|
||||
// TODO: navigare alla lista iscritti
|
||||
// $router.push('/members')
|
||||
};
|
||||
|
||||
// Wallet
|
||||
const refreshWallet = () => {
|
||||
@@ -289,8 +281,6 @@ export default defineComponent({
|
||||
goToProfile,
|
||||
sendRIS,
|
||||
receiveRIS,
|
||||
inviteFriend,
|
||||
showMembers,
|
||||
refreshWallet,
|
||||
goToTransactions,
|
||||
goToAllEvents,
|
||||
|
||||
@@ -173,130 +173,65 @@
|
||||
>
|
||||
<q-card class="annunci-dialog">
|
||||
<q-card-section class="dialog-header">
|
||||
<div class="dialog-title-row">
|
||||
<h3 class="dialog-title">Scegli categoria</h3>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
aria-label="Chiudi"
|
||||
@click="showAnnunciDialog = false"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-subtitle">Apri la sezione giusta in un tap.</div>
|
||||
<h3 class="dialog-title">Scegli Categoria</h3>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
v-close-popup
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
<q-card-section class="dialog-content">
|
||||
<div class="annunci-options-mobile">
|
||||
<div
|
||||
class="annuncio-option gradient-indigo"
|
||||
@click="goToGoods"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-tshirt"
|
||||
size="2.5rem"
|
||||
/>
|
||||
<span class="option-title">Beni</span>
|
||||
<span class="option-subtitle">Autoproduzioni · Cibo · Oggetti</span>
|
||||
</div>
|
||||
|
||||
<q-list class="annunci-list">
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
class="annunci-item gradient-green"
|
||||
@click="goToGoods"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<div class="annunci-icon">
|
||||
<q-icon
|
||||
name="fas fa-tshirt"
|
||||
size="1.4rem"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="option-title">Beni</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
class="option-subtitle"
|
||||
>
|
||||
Autoproduzioni · Cibo · Oggetti
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<div
|
||||
class="annuncio-option gradient-red"
|
||||
@click="goToServices"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-house-user"
|
||||
size="2.5rem"
|
||||
/>
|
||||
<span class="option-title">Servizi</span>
|
||||
<span class="option-subtitle">Competenze · Aiuti · Consulenze</span>
|
||||
</div>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
class="annunci-item gradient-red"
|
||||
@click="goToServices"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<div class="annunci-icon">
|
||||
<q-icon
|
||||
name="fas fa-house-user"
|
||||
size="1.4rem"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="option-title">Servizi</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
class="option-subtitle"
|
||||
>
|
||||
Competenze · Aiuti · Consulenze
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<div
|
||||
class="annuncio-option gradient-lime"
|
||||
@click="goToHospitality"
|
||||
>
|
||||
<q-icon
|
||||
name="fas fa-bed"
|
||||
size="2.5rem"
|
||||
/>
|
||||
<span class="option-title">Ospitalità</span>
|
||||
<span class="option-subtitle">Ospitare · Viaggi · Accoglienza</span>
|
||||
</div>
|
||||
|
||||
<q-item
|
||||
clickable
|
||||
v-ripple
|
||||
class="annunci-item gradient-lime"
|
||||
@click="goToHospitality"
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<div class="annunci-icon">
|
||||
<q-icon
|
||||
name="fas fa-bed"
|
||||
size="1.4rem"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="option-title">Ospitalità</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
class="option-subtitle"
|
||||
>
|
||||
Ospitare · Viaggi · Accoglienza
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-icon name="chevron_right" />
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
|
||||
<q-item
|
||||
class="annunci-item gradient-blue disabled"
|
||||
disable
|
||||
>
|
||||
<q-item-section avatar>
|
||||
<div class="annunci-icon">
|
||||
<q-icon
|
||||
name="commute"
|
||||
size="1.4rem"
|
||||
/>
|
||||
</div>
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label class="option-title">Trasporti</q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
class="option-subtitle"
|
||||
>
|
||||
In arrivo…
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
<div class="annuncio-option gradient-teal">
|
||||
<q-icon
|
||||
name="directions_car"
|
||||
size="2.5rem"
|
||||
/>
|
||||
<span class="option-title">Trasporti</span>
|
||||
<span class="option-subtitle">Condivisione viaggi</span>
|
||||
<span class="option-subtitle">⚠️ (IN ARRIVO...)</span>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
|
||||
/* =========================================================
|
||||
RISO Home (parte finale) — refresh estetico coerente
|
||||
========================================================= */
|
||||
|
||||
$space-xs: 4px;
|
||||
$space-sm: 8px;
|
||||
$space-md: 12px;
|
||||
$space-lg: 16px;
|
||||
$space-xl: 20px;
|
||||
$space-xxl: 24px;
|
||||
|
||||
|
||||
.riso-modern-home {
|
||||
padding: $s-md;
|
||||
padding-bottom: calc(#{$s-md} + env(safe-area-inset-bottom));
|
||||
@@ -28,6 +35,7 @@
|
||||
}
|
||||
|
||||
:global(body.body--dark) {
|
||||
|
||||
.content-section,
|
||||
.community-actions-section,
|
||||
.footer-section-modern {
|
||||
@@ -82,10 +90,12 @@
|
||||
&::-webkit-scrollbar {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
@@ -344,6 +354,7 @@
|
||||
/* ---- Footer ---- */
|
||||
.footer-section-modern {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $s-sm;
|
||||
|
||||
@media (min-width: 700px) {
|
||||
@@ -373,3 +384,159 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Community Actions Grid
|
||||
// ========================================
|
||||
.community-actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $space-md;
|
||||
margin-bottom: $space-lg;
|
||||
|
||||
// Mobile: colonna singola se necessario
|
||||
@media (max-width: 400px) {
|
||||
grid-template-columns: 1fr;
|
||||
gap: $space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.community-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $space-sm;
|
||||
padding: $space-md $space-lg;
|
||||
min-height: 48px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: none;
|
||||
box-shadow: var(--cm-shadow-sm);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--cm-shadow-md);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
// Quasar icon spacing fix
|
||||
:deep(.q-icon) {
|
||||
margin-right: $space-xs;
|
||||
}
|
||||
|
||||
// Dark mode adjustments
|
||||
.body--dark & {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Invite Friend Dialog
|
||||
// ========================================
|
||||
.invite-friend-dialog {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: $radius-xl;
|
||||
overflow: hidden;
|
||||
|
||||
// Mobile fullscreen
|
||||
@media (max-width: 599px) {
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// Header
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $space-lg $space-xl;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.invite {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #2e7d32 100%);
|
||||
|
||||
.body--dark & {
|
||||
background: linear-gradient(135deg, #388e3c 0%, #1b5e20 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-sm;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// Contenuto scrollabile
|
||||
.dialog-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: $space-lg;
|
||||
|
||||
// Scrollbar styling
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
.body--dark & {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.body--dark & {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Fix Dialog Background - Light/Dark
|
||||
// ========================================
|
||||
.q-dialog__inner>.invite-friend-dialog {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.body--dark {
|
||||
.q-dialog__inner>.invite-friend-dialog {
|
||||
background: #1e293b;
|
||||
|
||||
.dialog-content {
|
||||
background: #1e293b;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineComponent, ref, computed, watch, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { CRISBalanceBar } from '@/components/CRISBalanceBar';
|
||||
import { InvitaAmico } from '@/components/InvitaAmico';
|
||||
import { tools } from '@tools';
|
||||
import { useGlobalStore, useUserStore } from 'app/src/store';
|
||||
|
||||
@@ -8,7 +9,7 @@ const isTest = true; // Cambia a false in produzione
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Riso_Home_ParteFinale',
|
||||
components: { CRISBalanceBar },
|
||||
components: { CRISBalanceBar, InvitaAmico },
|
||||
setup() {
|
||||
const $router = useRouter();
|
||||
|
||||
@@ -16,6 +17,7 @@ export default defineComponent({
|
||||
const showAnnunciDialog = ref(false);
|
||||
const showBannScambio = ref(true);
|
||||
const walletSectionOpen = ref(false);
|
||||
const showInviteFriend = ref(false);
|
||||
|
||||
const selectedCircuit = ref<'provinciale' | 'italia'>('provinciale');
|
||||
const handshakesView = ref<'mine' | 'all'>('mine');
|
||||
@@ -138,11 +140,12 @@ export default defineComponent({
|
||||
|
||||
const inviteFriend = () => {
|
||||
// TODO: aprire dialog/pagina invito amico
|
||||
showInviteFriend.value = true
|
||||
};
|
||||
|
||||
const showMembers = () => {
|
||||
// TODO: navigare alla lista iscritti
|
||||
// $router.push('/members')
|
||||
$router.push('/friends')
|
||||
};
|
||||
|
||||
// Wallet
|
||||
@@ -210,6 +213,10 @@ export default defineComponent({
|
||||
// TODO: navigare a guida
|
||||
$router.push('/guida')
|
||||
};
|
||||
const openPresentazione = () => {
|
||||
// TODO: navigare a guida
|
||||
$router.push('/presentazione')
|
||||
};
|
||||
|
||||
const openInfo = () => {
|
||||
// TODO: navigare a info
|
||||
@@ -521,6 +528,7 @@ export default defineComponent({
|
||||
// $router.push('/circuits')
|
||||
};
|
||||
|
||||
|
||||
loadTestData();
|
||||
|
||||
onMounted(() => {
|
||||
@@ -609,6 +617,8 @@ export default defineComponent({
|
||||
allTransactions,
|
||||
userStore,
|
||||
walletSectionOpen,
|
||||
showInviteFriend,
|
||||
openPresentazione,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<q-page class="riso-modern-home riso-modern-home--tail">
|
||||
|
||||
<!-- Organizzazioni -->
|
||||
<section
|
||||
v-if="hasOrganizations"
|
||||
@@ -125,6 +124,43 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<q-dialog
|
||||
v-model="showInviteFriend"
|
||||
:maximized="$q.screen.lt.sm"
|
||||
transition-show="slide-up"
|
||||
transition-hide="slide-down"
|
||||
:persistent="false"
|
||||
>
|
||||
<q-card class="invite-friend-dialog">
|
||||
<!-- Header con pulsante chiusura -->
|
||||
<q-card-section class="dialog-header invite">
|
||||
<div class="dialog-title">
|
||||
<q-icon
|
||||
name="person_add"
|
||||
size="24px"
|
||||
/>
|
||||
<span>Invita Amici</span>
|
||||
</div>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="close"
|
||||
color="white"
|
||||
@click="showInviteFriend = false"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<!-- Contenuto scrollabile -->
|
||||
<q-card-section class="dialog-content">
|
||||
<InvitaAmico
|
||||
@invito-inviato="showInviteFriend = false"
|
||||
persistent
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Canali Telegram -->
|
||||
<!--<section
|
||||
v-if="hasTelegramLinks"
|
||||
@@ -186,10 +222,10 @@
|
||||
unelevated
|
||||
rounded
|
||||
class="footer-btn-modern"
|
||||
icon="help_outline"
|
||||
label="FAQ"
|
||||
color="primary"
|
||||
@click="openFAQ"
|
||||
icon="present_to_all"
|
||||
label="Presentazione"
|
||||
color="accent"
|
||||
@click="openPresentazione"
|
||||
/>
|
||||
<q-btn
|
||||
unelevated
|
||||
@@ -200,6 +236,15 @@
|
||||
color="secondary"
|
||||
@click="openGuide"
|
||||
/>
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
class="footer-btn-modern"
|
||||
icon="help_outline"
|
||||
label="Guida ai RIS"
|
||||
color="primary"
|
||||
@click="openFAQ"
|
||||
/>
|
||||
<q-btn
|
||||
unelevated
|
||||
rounded
|
||||
|
||||
@@ -998,15 +998,23 @@ $space-xxl: 24px;
|
||||
// ========================================
|
||||
// Send/Receive/Confirm Dialogs
|
||||
// ========================================
|
||||
// ========================================
|
||||
// Send/Receive/Confirm Dialogs - MODIFICATO
|
||||
// ========================================
|
||||
.send-dialog,
|
||||
.confirm-dialog {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
border-radius: $radius-xl;
|
||||
overflow: hidden;
|
||||
display: flex; // AGGIUNTO
|
||||
flex-direction: column; // AGGIUNTO
|
||||
max-height: 100vh; // AGGIUNTO
|
||||
|
||||
@media (max-width: 599px) {
|
||||
max-width: 100%;
|
||||
height: 100vh; // AGGIUNTO
|
||||
max-height: 100vh; // AGGIUNTO
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
@@ -1017,6 +1025,7 @@ $space-xxl: 24px;
|
||||
align-items: center;
|
||||
padding: $space-lg $space-xl;
|
||||
color: white;
|
||||
flex-shrink: 0; // AGGIUNTO - impedisce compressione
|
||||
|
||||
&.send {
|
||||
background: $gradient-send;
|
||||
@@ -1037,32 +1046,41 @@ $space-xxl: 24px;
|
||||
|
||||
.dialog-content {
|
||||
padding: $space-xl;
|
||||
flex: 1; // AGGIUNTO - occupa spazio disponibile
|
||||
overflow-y: auto; // AGGIUNTO - abilita scroll
|
||||
overflow-x: hidden; // AGGIUNTO
|
||||
|
||||
// AGGIUNTO - Scrollbar styling
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
padding: $space-md $space-xl $space-xl;
|
||||
flex-shrink: 0; // AGGIUNTO - impedisce compressione
|
||||
border-top: 1px solid var(--cm-border); // AGGIUNTO - separatore visivo
|
||||
}
|
||||
|
||||
.info-banner {
|
||||
display: flex;
|
||||
gap: $space-md;
|
||||
align-items: flex-start;
|
||||
padding: $space-md;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 1px solid rgba(59, 130, 246, 0.2);
|
||||
border-radius: $radius-md;
|
||||
margin-bottom: $space-lg;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
// Contacts
|
||||
// ========================================
|
||||
// Contacts Section - MODIFICATO
|
||||
// ========================================
|
||||
.contacts-section {
|
||||
margin-bottom: $space-lg;
|
||||
// Rimosso max-height - ora gestito dal parent
|
||||
|
||||
.contacts-title {
|
||||
display: flex;
|
||||
@@ -1074,14 +1092,45 @@ $space-xxl: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--cm-text-hint);
|
||||
margin-bottom: $space-md;
|
||||
position: sticky; // AGGIUNTO - titolo sticky
|
||||
top: 0; // AGGIUNTO
|
||||
background: var(--cm-bg-card); // AGGIUNTO
|
||||
padding: $space-sm 0; // AGGIUNTO
|
||||
z-index: 1; // AGGIUNTO
|
||||
}
|
||||
}
|
||||
|
||||
.contacts-scrollable {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
// Rimosso max-height e overflow - gestito dal parent .dialog-content
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-sm;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Dark Mode per Scrollbar - AGGIUNTO
|
||||
// ========================================
|
||||
.body--dark {
|
||||
.dialog-content {
|
||||
&::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contacts-section .contacts-title {
|
||||
background: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2237,4 +2286,5 @@ $space-xxl: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -999,6 +999,7 @@
|
||||
transition-hide="slide-down"
|
||||
>
|
||||
<q-card class="send-dialog">
|
||||
<!-- HEADER FISSO -->
|
||||
<q-card-section class="dialog-header send">
|
||||
<div class="dialog-title">
|
||||
<q-icon
|
||||
@@ -1017,123 +1018,151 @@
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<!-- CONTENUTO SCROLLABILE -->
|
||||
<q-card-section class="dialog-content">
|
||||
<!-- TOGGLE SELEZIONE VISTA CONTATTI -->
|
||||
<q-btn-toggle
|
||||
v-model="contactViewMode"
|
||||
spread
|
||||
no-caps
|
||||
rounded
|
||||
unelevated
|
||||
toggle-color="primary"
|
||||
color="grey-3"
|
||||
text-color="grey-8"
|
||||
class="contact-view-toggle q-mb-md"
|
||||
:options="contactViewOptions"
|
||||
/>
|
||||
|
||||
<!-- CAMPO DI RICERCA -->
|
||||
<q-input
|
||||
v-if="contactViewMode === 'recent'"
|
||||
v-model="sendSearch"
|
||||
label="Cerca destinatario..."
|
||||
:label="searchPlaceholder"
|
||||
outlined
|
||||
dense
|
||||
clearable
|
||||
class="search-input"
|
||||
@update:model-value="searchRecipients"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<template #prepend>
|
||||
<q-icon name="search" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<!-- Contatti recenti -->
|
||||
<!-- VISTA: CONTATTI RECENTI -->
|
||||
<div
|
||||
v-if="recentContacts.length && !sendSearch"
|
||||
v-if="contactViewMode === 'recent'"
|
||||
class="contacts-section"
|
||||
>
|
||||
<div class="contacts-title">
|
||||
<q-icon
|
||||
name="history"
|
||||
size="16px"
|
||||
/>
|
||||
<span>Usati di recente</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="contact in recentContacts"
|
||||
:key="contact.username"
|
||||
class="contact-item recent"
|
||||
@click="selectRecipient(contact)"
|
||||
>
|
||||
<q-avatar
|
||||
round
|
||||
size="44px"
|
||||
>
|
||||
<img :src="userStore.getImgByProfile(contact)" />
|
||||
<q-badge
|
||||
v-if="tools.isUserOnline(contact)"
|
||||
floating
|
||||
color="green"
|
||||
rounded
|
||||
/>
|
||||
</q-avatar>
|
||||
<div class="contact-info">
|
||||
<div
|
||||
v-if="contact.name"
|
||||
class="contact-name"
|
||||
>
|
||||
{{ contact.name }}
|
||||
<span v-if="contact.surname">{{ contact.surname }}</span>
|
||||
</div>
|
||||
<div class="contact-username">@{{ contact.username }}</div>
|
||||
</div>
|
||||
<q-icon
|
||||
name="chevron_right"
|
||||
size="20px"
|
||||
color="grey-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Risultati ricerca -->
|
||||
<div
|
||||
v-if="sendSearch && filteredContacts.length"
|
||||
class="contacts-section"
|
||||
>
|
||||
<div class="contacts-title">
|
||||
<q-icon
|
||||
name="people"
|
||||
size="16px"
|
||||
/>
|
||||
<span>Risultati ricerca</span>
|
||||
</div>
|
||||
<div class="contacts-scrollable">
|
||||
<div
|
||||
v-for="contact in filteredContacts"
|
||||
:key="contact.id"
|
||||
class="contact-item"
|
||||
@click="selectRecipient(contact)"
|
||||
>
|
||||
<q-avatar
|
||||
size="44px"
|
||||
:color="contact.avatarColor"
|
||||
text-color="white"
|
||||
>
|
||||
{{ contact.initials }}
|
||||
</q-avatar>
|
||||
<div class="contact-info">
|
||||
<div class="contact-name">{{ contact.name }}</div>
|
||||
<div class="contact-username">@{{ contact.username }}</div>
|
||||
</div>
|
||||
<!-- Lista contatti recenti -->
|
||||
<template v-if="filteredRecentContacts.length">
|
||||
<div class="contacts-title">
|
||||
<q-icon
|
||||
name="chevron_right"
|
||||
size="20px"
|
||||
color="grey-6"
|
||||
name="history"
|
||||
size="16px"
|
||||
/>
|
||||
<span>Usati di recente</span>
|
||||
<q-badge
|
||||
color="primary"
|
||||
:label="filteredRecentContacts.length"
|
||||
class="q-ml-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="contacts-scrollable">
|
||||
<div
|
||||
v-for="contact in filteredRecentContacts"
|
||||
:key="contact.username"
|
||||
class="contact-item recent"
|
||||
@click="selectRecipient(contact)"
|
||||
>
|
||||
<q-avatar
|
||||
round
|
||||
size="44px"
|
||||
>
|
||||
<img :src="userStore.getImgByProfile(contact)" />
|
||||
<q-badge
|
||||
v-if="tools.isUserOnline(contact)"
|
||||
floating
|
||||
color="green"
|
||||
rounded
|
||||
/>
|
||||
</q-avatar>
|
||||
<div class="contact-info">
|
||||
<div
|
||||
v-if="contact.name"
|
||||
class="contact-name"
|
||||
>
|
||||
{{ contact.name }}
|
||||
<span v-if="contact.surname">{{ contact.surname }}</span>
|
||||
</div>
|
||||
<div class="contact-username">@{{ contact.username }}</div>
|
||||
</div>
|
||||
<q-icon
|
||||
name="chevron_right"
|
||||
size="20px"
|
||||
color="grey-6"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Nessun contatto recente -->
|
||||
<div
|
||||
v-else
|
||||
class="no-results"
|
||||
>
|
||||
<q-icon
|
||||
:name="sendSearch ? 'search_off' : 'history'"
|
||||
size="56px"
|
||||
color="grey-5"
|
||||
/>
|
||||
<p>
|
||||
{{
|
||||
sendSearch
|
||||
? 'Nessun contatto recente trovato'
|
||||
: 'Nessun contatto recente'
|
||||
}}
|
||||
</p>
|
||||
<q-btn
|
||||
v-if="!sendSearch"
|
||||
flat
|
||||
color="primary"
|
||||
label="Cerca in tutti i contatti"
|
||||
icon="people"
|
||||
no-caps
|
||||
@click="contactViewMode = 'all'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- VISTA: TUTTI I CONTATTI -->
|
||||
<div
|
||||
v-if="sendSearch && !filteredContacts.length"
|
||||
class="no-results"
|
||||
v-else-if="contactViewMode === 'all'"
|
||||
class="contacts-section"
|
||||
>
|
||||
<q-icon
|
||||
name="search_off"
|
||||
size="56px"
|
||||
color="grey-5"
|
||||
/>
|
||||
<p>Nessun contatto trovato</p>
|
||||
<CTitleBanner
|
||||
class="q-pa-xs"
|
||||
:title="$t('circuit.sendcoins')"
|
||||
bgcolor="white"
|
||||
bgcolor2="lightblue"
|
||||
clcolor="text-indigo"
|
||||
:canopen="true"
|
||||
:small="true"
|
||||
:open="true"
|
||||
>
|
||||
<CFindUsers
|
||||
v-show="contactViewMode === 'all'"
|
||||
:actionType="costanti.ACTIONTYPE.SEND_RIS"
|
||||
:enableContactClick="true"
|
||||
@clickContact="clickContact"
|
||||
/>
|
||||
</CTitleBanner>
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<CSendCoins
|
||||
v-if="showReceiveDialog"
|
||||
mode="receive"
|
||||
@@ -1238,6 +1267,7 @@ import { CRISBalanceBar } from '@/components/CRISBalanceBar';
|
||||
import { CSingleMovement } from '@/components/CSingleMovement';
|
||||
import { CSendCoins } from '@/components/CSendCoins';
|
||||
import { CTitleBanner } from '@/components/CTitleBanner';
|
||||
import { CFindUsers } from '@/components/CFindUsers';
|
||||
import { CQRCode } from '@/components/CQRCode';
|
||||
import { CCopyBtnSmall } from '@/components/CCopyBtnSmall';
|
||||
import { CFinder } from '@/components/CFinder';
|
||||
@@ -1272,6 +1302,61 @@ const exploreSearch = ref('');
|
||||
// Transactions
|
||||
const allTransactions = ref<IMovVisu[]>([]);
|
||||
|
||||
// ============================================
|
||||
// STATE
|
||||
// ============================================
|
||||
const contactViewMode = ref('recent'); // 'recent' | 'all'
|
||||
const sendSearch = ref('');
|
||||
|
||||
// ============================================
|
||||
// COMPUTED
|
||||
// ============================================
|
||||
|
||||
// Opzioni per il toggle
|
||||
const contactViewOptions = computed(() => [
|
||||
{
|
||||
label: 'Recenti',
|
||||
value: 'recent',
|
||||
icon: 'history',
|
||||
slot: 'recent',
|
||||
},
|
||||
{
|
||||
label: 'Tutti',
|
||||
value: 'all',
|
||||
icon: 'people',
|
||||
slot: 'all',
|
||||
},
|
||||
]);
|
||||
|
||||
// Placeholder dinamico per la ricerca
|
||||
const searchPlaceholder = computed(() => {
|
||||
return contactViewMode.value === 'recent'
|
||||
? 'Cerca nei contatti recenti...'
|
||||
: 'Cerca in tutti i contatti...';
|
||||
});
|
||||
|
||||
// Contatti recenti filtrati
|
||||
const filteredRecentContacts = computed(() => {
|
||||
if (!sendSearch.value) {
|
||||
return recentContacts.value;
|
||||
}
|
||||
|
||||
const search = sendSearch.value.toLowerCase().trim();
|
||||
|
||||
return recentContacts.value.filter((contact) => {
|
||||
const name = contact.name?.toLowerCase() || '';
|
||||
const surname = contact.surname?.toLowerCase() || '';
|
||||
const username = contact.username?.toLowerCase() || '';
|
||||
|
||||
return (
|
||||
name.includes(search) ||
|
||||
surname.includes(search) ||
|
||||
username.includes(search) ||
|
||||
`${name} ${surname}`.includes(search)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function configureShowcase() {
|
||||
// Naviga alla configurazione vetrina
|
||||
closeReceiveDialog();
|
||||
@@ -1400,7 +1485,6 @@ const showConfirmSendDialog = ref(false);
|
||||
const showConfirmReceiveDialog = ref(false);
|
||||
|
||||
// Send states
|
||||
const sendSearch = ref('');
|
||||
const selectedRecipient = ref<any>(null);
|
||||
|
||||
const filteredContacts = computed(() => {
|
||||
@@ -1694,8 +1778,8 @@ const getOtherUserUsername = computed(() => {
|
||||
if (!selectedMov.value) return '';
|
||||
// Adatta in base alla tua struttura dati
|
||||
return isSelectedMovSent.value
|
||||
? selectedMov.value.to_username || getOtherUserName.value
|
||||
: selectedMov.value.from_username || getOtherUserName.value;
|
||||
? selectedMov.value.userto.username || getOtherUserName.value
|
||||
: selectedMov.value.userfrom.username || getOtherUserName.value;
|
||||
});
|
||||
|
||||
const getOtherUserAvatar = computed(() => {
|
||||
@@ -1744,6 +1828,15 @@ const openPresentazione = () => {
|
||||
const openGuidaRIS = () => {
|
||||
$router.push('/faq_ris');
|
||||
};
|
||||
|
||||
const clickContact = (data: any) => {
|
||||
const recuser = data.recuser;
|
||||
if (recuser) {
|
||||
selectRecipient(recuser);
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
150
src/components/video/VideoGallery.scss
Normal file
150
src/components/video/VideoGallery.scss
Normal file
@@ -0,0 +1,150 @@
|
||||
.video-gallery {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
.breadcrumb-section {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Folder Cards
|
||||
.folder-card {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Video Cards
|
||||
.video-card {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
padding-top: 56.25%; // 16:9
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.play-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// List View
|
||||
.video-list {
|
||||
.video-list-item {
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbnail-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
// Dialogs
|
||||
.video-player-dialog {
|
||||
background: #000;
|
||||
|
||||
.video-player-container {
|
||||
height: calc(100vh - 50px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-move {
|
||||
min-width: 350px;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Mode
|
||||
.body--dark {
|
||||
.video-gallery {
|
||||
.breadcrumb-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.video-list-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/components/video/VideoGallery.ts
Normal file
291
src/components/video/VideoGallery.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { videoService } from '@/services/videoService';
|
||||
import type { IVideo, IFolder, IFolderOption } from '@/types/video.types';
|
||||
|
||||
interface IBreadcrumb {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoGallery',
|
||||
|
||||
props: {
|
||||
refreshTrigger: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const $q = useQuasar();
|
||||
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const currentPath = ref('');
|
||||
const videos = ref<IVideo[]>([]);
|
||||
const subfolders = ref<IFolder[]>([]);
|
||||
const allFolders = ref<IFolderOption[]>([]);
|
||||
const viewMode = ref<'grid' | 'list'>('grid');
|
||||
|
||||
// Video player
|
||||
const showVideoDialog = ref(false);
|
||||
const currentVideo = ref<IVideo | null>(null);
|
||||
const videoPlayer = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
// Move dialog
|
||||
const showMoveDialog = ref(false);
|
||||
const moveDestination = ref('');
|
||||
const videoToMove = ref<IVideo | null>(null);
|
||||
const moving = ref(false);
|
||||
|
||||
// Computed
|
||||
const breadcrumbs = computed<IBreadcrumb[]>(() => {
|
||||
if (!currentPath.value) return [];
|
||||
|
||||
const parts = currentPath.value.split('/');
|
||||
return parts.map((part, index) => ({
|
||||
name: part,
|
||||
path: parts.slice(0, index + 1).join('/')
|
||||
}));
|
||||
});
|
||||
|
||||
// Methods
|
||||
const loadContent = async (): Promise<void> => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await videoService.getVideos(currentPath.value);
|
||||
videos.value = response.data?.videos || [];
|
||||
subfolders.value = response.data?.folders || [];
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nel caricamento dei contenuti'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadAllFolders = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await videoService.getFolders();
|
||||
const folders = response.data?.folders || [];
|
||||
allFolders.value = [
|
||||
{ label: 'Root', value: '' },
|
||||
...folders.map(f => ({
|
||||
label: f.path,
|
||||
value: f.path
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Error loading folders:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateTo = (path: string): void => {
|
||||
currentPath.value = path;
|
||||
loadContent();
|
||||
};
|
||||
|
||||
const toggleViewMode = (): void => {
|
||||
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid';
|
||||
};
|
||||
|
||||
const openVideo = (video: IVideo): void => {
|
||||
currentVideo.value = video;
|
||||
showVideoDialog.value = true;
|
||||
};
|
||||
|
||||
const getVideoUrl = (path: string): string => {
|
||||
return videoService.getVideoUrl(path);
|
||||
};
|
||||
|
||||
const downloadVideo = (video: IVideo): void => {
|
||||
const link = document.createElement('a');
|
||||
link.href = getVideoUrl(video.path);
|
||||
link.download = video.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const renameVideo = (video: IVideo): void => {
|
||||
$q.dialog({
|
||||
title: 'Rinomina Video',
|
||||
message: 'Inserisci il nuovo nome del file:',
|
||||
prompt: {
|
||||
model: video.filename,
|
||||
type: 'text'
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async (newName: string) => {
|
||||
if (!newName.trim() || newName === video.filename) return;
|
||||
|
||||
try {
|
||||
await videoService.renameVideo(video.folder || currentPath.value, video.filename, newName);
|
||||
$q.notify({ type: 'positive', message: 'Video rinominato!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante la rinomina'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const moveVideo = async (video: IVideo): Promise<void> => {
|
||||
await loadAllFolders();
|
||||
videoToMove.value = video;
|
||||
moveDestination.value = '';
|
||||
showMoveDialog.value = true;
|
||||
};
|
||||
|
||||
const confirmMoveVideo = async (): Promise<void> => {
|
||||
if (!videoToMove.value) return;
|
||||
|
||||
moving.value = true;
|
||||
try {
|
||||
await videoService.moveVideo(
|
||||
videoToMove.value.folder || currentPath.value,
|
||||
videoToMove.value.filename,
|
||||
moveDestination.value
|
||||
);
|
||||
$q.notify({ type: 'positive', message: 'Video spostato!' });
|
||||
showMoveDialog.value = false;
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante lo spostamento'
|
||||
});
|
||||
} finally {
|
||||
moving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteVideo = (video: IVideo): void => {
|
||||
$q.dialog({
|
||||
title: 'Conferma Eliminazione',
|
||||
message: `Sei sicuro di voler eliminare "${video.filename}"?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
color: 'negative'
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await videoService.deleteVideo(video.folder || currentPath.value, video.filename);
|
||||
$q.notify({ type: 'positive', message: 'Video eliminato!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renameFolder = (folder: IFolder): void => {
|
||||
$q.dialog({
|
||||
title: 'Rinomina Cartella',
|
||||
message: 'Inserisci il nuovo nome:',
|
||||
prompt: {
|
||||
model: folder.name,
|
||||
type: 'text'
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async (newName: string) => {
|
||||
if (!newName.trim() || newName === folder.name) return;
|
||||
|
||||
try {
|
||||
await videoService.renameFolder(folder.path, newName);
|
||||
$q.notify({ type: 'positive', message: 'Cartella rinominata!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante la rinomina'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDeleteFolder = (folder: IFolder): void => {
|
||||
$q.dialog({
|
||||
title: 'Conferma Eliminazione',
|
||||
message: `Sei sicuro di voler eliminare la cartella "${folder.name}" e tutto il suo contenuto?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
color: 'negative'
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await videoService.deleteFolder(folder.path);
|
||||
$q.notify({ type: 'positive', message: 'Cartella eliminata!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
return videoService.formatFileSize(bytes);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return videoService.formatDate(dateString);
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(() => props.refreshTrigger, () => {
|
||||
loadContent();
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadContent();
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
currentPath,
|
||||
videos,
|
||||
subfolders,
|
||||
allFolders,
|
||||
viewMode,
|
||||
showVideoDialog,
|
||||
currentVideo,
|
||||
videoPlayer,
|
||||
showMoveDialog,
|
||||
moveDestination,
|
||||
moving,
|
||||
|
||||
// Computed
|
||||
breadcrumbs,
|
||||
|
||||
// Methods
|
||||
loadContent,
|
||||
navigateTo,
|
||||
toggleViewMode,
|
||||
openVideo,
|
||||
getVideoUrl,
|
||||
downloadVideo,
|
||||
renameVideo,
|
||||
moveVideo,
|
||||
confirmMoveVideo,
|
||||
confirmDeleteVideo,
|
||||
renameFolder,
|
||||
confirmDeleteFolder,
|
||||
formatFileSize,
|
||||
formatDate
|
||||
};
|
||||
}
|
||||
});
|
||||
271
src/components/video/VideoGallery.vue
Normal file
271
src/components/video/VideoGallery.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<q-card class="video-gallery">
|
||||
<q-card-section>
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-h6">
|
||||
<q-icon name="video_library" class="q-mr-sm" />
|
||||
Galleria Video
|
||||
</div>
|
||||
<div class="row q-gutter-sm">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
:icon="viewMode === 'grid' ? 'view_list' : 'grid_view'"
|
||||
@click="toggleViewMode"
|
||||
>
|
||||
<q-tooltip>Cambia vista</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="refresh"
|
||||
:loading="loading"
|
||||
@click="loadContent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<q-card-section class="q-py-sm breadcrumb-section">
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el
|
||||
icon="home"
|
||||
label="Root"
|
||||
class="cursor-pointer"
|
||||
@click="navigateTo('')"
|
||||
/>
|
||||
<q-breadcrumbs-el
|
||||
v-for="(crumb, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:label="crumb.name"
|
||||
class="cursor-pointer"
|
||||
@click="navigateTo(crumb.path)"
|
||||
/>
|
||||
</q-breadcrumbs>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="text-center q-pa-lg">
|
||||
<q-spinner-orbit size="50px" color="primary" />
|
||||
<div class="q-mt-md">Caricamento...</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else>
|
||||
<!-- Folders -->
|
||||
<div v-if="subfolders.length > 0" class="q-mb-lg">
|
||||
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
|
||||
<q-icon name="folder" class="q-mr-xs" />
|
||||
Cartelle
|
||||
</div>
|
||||
<div class="row q-gutter-md">
|
||||
<div
|
||||
v-for="folder in subfolders"
|
||||
:key="folder.path"
|
||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
||||
>
|
||||
<q-card
|
||||
class="folder-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="navigateTo(folder.path)"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="folder" size="48px" color="amber" />
|
||||
<div class="text-subtitle2 q-mt-sm ellipsis">
|
||||
{{ folder.name }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-menu touch-position context-menu>
|
||||
<q-list dense>
|
||||
<q-item v-close-popup clickable @click="renameFolder(folder)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="edit" color="primary" />
|
||||
</q-item-section>
|
||||
<q-item-section>Rinomina</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-close-popup clickable @click="confirmDeleteFolder(folder)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete" color="negative" />
|
||||
</q-item-section>
|
||||
<q-item-section>Elimina</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Videos -->
|
||||
<div v-if="videos.length > 0">
|
||||
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
|
||||
<q-icon name="movie" class="q-mr-xs" />
|
||||
Video ({{ videos.length }})
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div v-if="viewMode === 'grid'" class="row q-gutter-md">
|
||||
<div
|
||||
v-for="video in videos"
|
||||
:key="video.id"
|
||||
class="col-12 col-sm-6 col-md-4 col-lg-3"
|
||||
>
|
||||
<q-card class="video-card">
|
||||
<div class="video-container" @click="openVideo(video)">
|
||||
<video
|
||||
:src="getVideoUrl(video.path)"
|
||||
class="video-preview"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div class="play-overlay">
|
||||
<q-icon name="play_circle" size="64px" color="white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-card-section class="q-py-sm">
|
||||
<div class="text-subtitle2 ellipsis">{{ video.filename }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
{{ formatFileSize(video.size) }} •
|
||||
{{ formatDate(video.createdAt) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn flat round icon="play_arrow" color="primary" @click="openVideo(video)">
|
||||
<q-tooltip>Riproduci</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round icon="download" color="secondary" @click="downloadVideo(video)">
|
||||
<q-tooltip>Scarica</q-tooltip>
|
||||
</q-btn>
|
||||
<q-space />
|
||||
<q-btn flat round icon="more_vert">
|
||||
<q-menu>
|
||||
<q-list dense>
|
||||
<q-item v-close-popup clickable @click="renameVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Rinomina</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-close-popup clickable @click="moveVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="drive_file_move" />
|
||||
</q-item-section>
|
||||
<q-item-section>Sposta</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-close-popup clickable @click="confirmDeleteVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete" color="negative" />
|
||||
</q-item-section>
|
||||
<q-item-section class="text-negative">Elimina</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<q-list v-else separator class="video-list">
|
||||
<q-item v-for="video in videos" :key="video.id" class="video-list-item">
|
||||
<q-item-section avatar>
|
||||
<q-avatar square size="60px" class="video-thumbnail">
|
||||
<video :src="getVideoUrl(video.path)" preload="metadata" />
|
||||
<div class="thumbnail-overlay">
|
||||
<q-icon name="play_arrow" color="white" />
|
||||
</div>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ video.filename }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ formatFileSize(video.size) }} • {{ formatDate(video.createdAt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row q-gutter-xs">
|
||||
<q-btn flat round dense icon="play_arrow" color="primary" @click="openVideo(video)" />
|
||||
<q-btn flat round dense icon="download" color="secondary" @click="downloadVideo(video)" />
|
||||
<q-btn flat round dense icon="delete" color="negative" @click="confirmDeleteVideo(video)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && videos.length === 0 && subfolders.length === 0" class="empty-state">
|
||||
<q-icon name="folder_off" size="80px" color="grey-5" />
|
||||
<div class="text-h6 text-grey-6 q-mt-md">Nessun contenuto</div>
|
||||
<div class="text-grey-5">
|
||||
Questa cartella è vuota. Carica dei video per iniziare.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- Video Player Dialog -->
|
||||
<q-dialog v-model="showVideoDialog" maximized transition-show="fade" transition-hide="fade">
|
||||
<q-card class="video-player-dialog">
|
||||
<q-bar class="bg-grey-9">
|
||||
<div class="text-white ellipsis">{{ currentVideo?.filename }}</div>
|
||||
<q-space />
|
||||
<q-btn v-close-popup dense flat icon="close" color="white" />
|
||||
</q-bar>
|
||||
|
||||
<q-card-section class="video-player-container">
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
ref="videoPlayer"
|
||||
:src="getVideoUrl(currentVideo.path)"
|
||||
controls
|
||||
autoplay
|
||||
class="video-player"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Move Video Dialog -->
|
||||
<q-dialog v-model="showMoveDialog" persistent>
|
||||
<q-card class="dialog-move">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Sposta Video</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-select
|
||||
v-model="moveDestination"
|
||||
:options="allFolders"
|
||||
label="Cartella di destinazione"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat label="Annulla" />
|
||||
<q-btn color="primary" label="Sposta" :loading="moving" @click="confirmMoveVideo" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideoGallery.ts" />
|
||||
<style lang="scss" src="./VideoGallery.scss" scoped />
|
||||
73
src/components/video/VideoUploader.scss
Normal file
73
src/components/video/VideoUploader.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
.video-uploader {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
.btn-new-folder {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
min-height: 120px;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
|
||||
&:deep(.q-field__control) {
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-chip {
|
||||
max-width: 100%;
|
||||
|
||||
.ellipsis {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-queue {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 8px;
|
||||
|
||||
.q-item {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-new-folder {
|
||||
min-width: 350px;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
.body--dark {
|
||||
.video-uploader {
|
||||
.dropzone {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-queue {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
.q-item {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/components/video/VideoUploader.ts
Normal file
199
src/components/video/VideoUploader.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { videoService } from '@/services/videoService';
|
||||
import {
|
||||
IFolder,
|
||||
IFolderOption,
|
||||
IUploadQueueItem,
|
||||
UploadStatus,
|
||||
UPLOAD_STATUS_CONFIG,
|
||||
MAX_FILES
|
||||
} from '@/types/video.types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoUploader',
|
||||
|
||||
emits: ['upload-complete'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const $q = useQuasar();
|
||||
|
||||
// State
|
||||
const selectedFiles = ref<File[] | null>(null);
|
||||
const selectedFolder = ref<string>('');
|
||||
const folders = ref<IFolder[]>([]);
|
||||
const loadingFolders = ref(false);
|
||||
const uploadQueue = ref<IUploadQueueItem[]>([]);
|
||||
const isUploading = ref(false);
|
||||
|
||||
// Dialog state
|
||||
const showNewFolderDialog = ref(false);
|
||||
const newFolderName = ref('');
|
||||
const newFolderParent = ref('');
|
||||
const creatingFolder = ref(false);
|
||||
|
||||
// Constants
|
||||
const maxFiles = MAX_FILES;
|
||||
|
||||
// Computed
|
||||
const folderOptions = computed<IFolderOption[]>(() => [
|
||||
{ label: '📁 Root (principale)', value: '' },
|
||||
...folders.value.map(f => ({
|
||||
label: `${' '.repeat((f.level || 1) - 1)}📂 ${f.name}`,
|
||||
value: f.path
|
||||
}))
|
||||
]);
|
||||
|
||||
const parentFolderOptions = computed<IFolderOption[]>(() => [
|
||||
{ label: 'Root', value: '' },
|
||||
...folderOptions.value.slice(1)
|
||||
]);
|
||||
|
||||
// Methods
|
||||
const loadFolders = async (): Promise<void> => {
|
||||
loadingFolders.value = true;
|
||||
try {
|
||||
const response = await videoService.getFolders();
|
||||
folders.value = response.data?.folders || [];
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nel caricamento delle cartelle'
|
||||
});
|
||||
} finally {
|
||||
loadingFolders.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onFilesSelected = (files: File[] | null): void => {
|
||||
if (!files) return;
|
||||
|
||||
const fileArray = Array.isArray(files) ? files : [files];
|
||||
uploadQueue.value = fileArray.map(file => ({
|
||||
file,
|
||||
progress: 0,
|
||||
status: 'pending' as UploadStatus
|
||||
}));
|
||||
};
|
||||
|
||||
const startUpload = async (): Promise<void> => {
|
||||
if (uploadQueue.value.length === 0) return;
|
||||
|
||||
isUploading.value = true;
|
||||
let completedCount = 0;
|
||||
|
||||
for (const item of uploadQueue.value) {
|
||||
if (item.status === 'complete') continue;
|
||||
|
||||
item.status = 'uploading';
|
||||
|
||||
try {
|
||||
await videoService.uploadVideo(
|
||||
item.file,
|
||||
selectedFolder.value || 'default',
|
||||
(progress: number) => {
|
||||
item.progress = progress;
|
||||
}
|
||||
);
|
||||
item.status = 'complete';
|
||||
item.progress = 100;
|
||||
completedCount++;
|
||||
} catch (error: any) {
|
||||
item.status = 'error';
|
||||
item.error = error.response?.data?.error || error.message;
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: `Errore upload ${item.file.name}: ${item.error}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isUploading.value = false;
|
||||
|
||||
if (completedCount > 0) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `${completedCount} video caricati con successo!`
|
||||
});
|
||||
emit('upload-complete');
|
||||
}
|
||||
};
|
||||
|
||||
const createNewFolder = async (): Promise<void> => {
|
||||
if (!newFolderName.value.trim()) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Inserisci un nome per la cartella'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
creatingFolder.value = true;
|
||||
try {
|
||||
await videoService.createFolder(newFolderName.value, newFolderParent.value);
|
||||
$q.notify({ type: 'positive', message: 'Cartella creata!' });
|
||||
|
||||
await loadFolders();
|
||||
|
||||
selectedFolder.value = newFolderParent.value
|
||||
? `${newFolderParent.value}/${newFolderName.value}`
|
||||
: newFolderName.value;
|
||||
|
||||
showNewFolderDialog.value = false;
|
||||
newFolderName.value = '';
|
||||
newFolderParent.value = '';
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore nella creazione della cartella'
|
||||
});
|
||||
} finally {
|
||||
creatingFolder.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearQueue = (): void => {
|
||||
uploadQueue.value = [];
|
||||
selectedFiles.value = null;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
return videoService.formatFileSize(bytes);
|
||||
};
|
||||
|
||||
const getStatusConfig = (status: UploadStatus) => {
|
||||
return UPLOAD_STATUS_CONFIG[status];
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadFolders();
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedFiles,
|
||||
selectedFolder,
|
||||
loadingFolders,
|
||||
uploadQueue,
|
||||
isUploading,
|
||||
showNewFolderDialog,
|
||||
newFolderName,
|
||||
newFolderParent,
|
||||
creatingFolder,
|
||||
maxFiles,
|
||||
|
||||
// Computed
|
||||
folderOptions,
|
||||
parentFolderOptions,
|
||||
|
||||
// Methods
|
||||
onFilesSelected,
|
||||
startUpload,
|
||||
createNewFolder,
|
||||
clearQueue,
|
||||
formatFileSize,
|
||||
getStatusConfig
|
||||
};
|
||||
}
|
||||
});
|
||||
181
src/components/video/VideoUploader.vue
Normal file
181
src/components/video/VideoUploader.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<q-card class="video-uploader">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<q-icon name="cloud_upload" class="q-mr-sm" />
|
||||
Carica Video
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- Selezione Cartella -->
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="selectedFolder"
|
||||
:options="folderOptions"
|
||||
label="Seleziona Cartella di Destinazione"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:loading="loadingFolders"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="folder" />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="folder" color="amber" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="create_new_folder"
|
||||
label="Nuova Cartella"
|
||||
class="full-width btn-new-folder"
|
||||
@click="showNewFolderDialog = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropzone -->
|
||||
<q-file
|
||||
v-model="selectedFiles"
|
||||
label="Trascina i video qui o clicca per selezionare"
|
||||
outlined
|
||||
multiple
|
||||
counter
|
||||
accept="video/*"
|
||||
:max-files="maxFiles"
|
||||
class="dropzone"
|
||||
@update:model-value="onFilesSelected"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="videocam" />
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
<q-chip class="full-width q-my-xs file-chip" square>
|
||||
<q-avatar>
|
||||
<q-icon name="movie" />
|
||||
</q-avatar>
|
||||
<div class="ellipsis relative-position">
|
||||
{{ file.name }}
|
||||
<q-tooltip>{{ file.name }}</q-tooltip>
|
||||
</div>
|
||||
<q-chip dense class="q-ml-sm" color="primary" text-color="white">
|
||||
{{ formatFileSize(file.size) }}
|
||||
</q-chip>
|
||||
</q-chip>
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<!-- Lista Upload Queue -->
|
||||
<q-list v-if="uploadQueue.length > 0" class="q-mt-md upload-queue">
|
||||
<q-item v-for="(item, index) in uploadQueue" :key="index">
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="getStatusConfig(item.status).icon"
|
||||
:color="getStatusConfig(item.status).color"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ item.file.name }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ formatFileSize(item.file.size) }}
|
||||
</q-item-label>
|
||||
<q-linear-progress
|
||||
v-if="item.status === 'uploading'"
|
||||
:value="item.progress / 100"
|
||||
color="primary"
|
||||
class="q-mt-sm"
|
||||
/>
|
||||
<q-item-label v-if="item.error" caption class="text-negative">
|
||||
{{ item.error }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-badge
|
||||
:color="getStatusConfig(item.status).color"
|
||||
:label="getStatusConfig(item.status).label"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
label="Pulisci"
|
||||
color="grey"
|
||||
:disable="isUploading"
|
||||
@click="clearQueue"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="cloud_upload"
|
||||
label="Carica Video"
|
||||
:loading="isUploading"
|
||||
:disable="uploadQueue.length === 0 || !selectedFolder"
|
||||
@click="startUpload"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
<!-- Dialog Nuova Cartella -->
|
||||
<q-dialog v-model="showNewFolderDialog" persistent>
|
||||
<q-card class="dialog-new-folder">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Nuova Cartella</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-input
|
||||
v-model="newFolderName"
|
||||
label="Nome Cartella"
|
||||
autofocus
|
||||
outlined
|
||||
@keyup.enter="createNewFolder"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-model="newFolderParent"
|
||||
:options="parentFolderOptions"
|
||||
label="Cartella Padre (opzionale)"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
class="q-mt-md"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat label="Annulla" />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Crea"
|
||||
:loading="creatingFolder"
|
||||
@click="createNewFolder"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideoUploader.ts" />
|
||||
<style lang="scss" src="./VideoUploader.scss" scoped />
|
||||
@@ -85,6 +85,7 @@ const msg_website_it = {
|
||||
eventodef: 'Evento:',
|
||||
prova: 'prova',
|
||||
dbop: 'Operazioni',
|
||||
VideoPage: 'Video',
|
||||
projall: 'Comunitari',
|
||||
groups: 'Lista Gruppi',
|
||||
projectsShared: 'Condivisi da me',
|
||||
|
||||
@@ -250,6 +250,7 @@ export interface IUserFields {
|
||||
made_gift?: boolean
|
||||
tokens?: IToken[]
|
||||
date_reg?: Date
|
||||
date_deleted?: Date
|
||||
lasttimeonline?: Date
|
||||
profile: IUserProfile
|
||||
qualified?: boolean
|
||||
|
||||
16
src/pages/VideosPage.scss
Normal file
16
src/pages/VideosPage.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.videos-page {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.body--dark {
|
||||
.videos-page {
|
||||
background: #1d1d1d;
|
||||
}
|
||||
}
|
||||
25
src/pages/VideosPage.ts
Normal file
25
src/pages/VideosPage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import VideoUploader from '@/components/video/VideoUploader.vue';
|
||||
import VideoGallery from '@/components/video/VideoGallery.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideosPage',
|
||||
|
||||
components: {
|
||||
VideoUploader,
|
||||
VideoGallery
|
||||
},
|
||||
|
||||
setup() {
|
||||
const refreshTrigger = ref(0);
|
||||
|
||||
const onUploadComplete = (): void => {
|
||||
refreshTrigger.value++;
|
||||
};
|
||||
|
||||
return {
|
||||
refreshTrigger,
|
||||
onUploadComplete
|
||||
};
|
||||
}
|
||||
});
|
||||
11
src/pages/VideosPage.vue
Normal file
11
src/pages/VideosPage.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<q-page class="videos-page">
|
||||
<div class="container">
|
||||
<VideoUploader @upload-complete="onUploadComplete" />
|
||||
<VideoGallery :refresh-trigger="refreshTrigger" class="q-mt-lg" />
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideosPage.ts" />
|
||||
<style lang="scss" src="./VideosPage.scss" scoped />
|
||||
@@ -196,6 +196,15 @@
|
||||
mykey="Email"
|
||||
:myvalue="myuser.email"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
mykey="Registrata il"
|
||||
:mydate="myuser.date_reg"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
v-if="myuser.date_deleted"
|
||||
mykey="Cancellato il"
|
||||
:mydate="myuser.date_deleted"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
mykey="Email Verificata"
|
||||
:myvalue="myuser.verified_email"
|
||||
|
||||
@@ -43,6 +43,19 @@ function getRoutesAd(site: ISites) {
|
||||
submenu: true,
|
||||
onlyAdmin: true
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 125,
|
||||
path: '/admin/videos',
|
||||
materialIcon: 'fas fa-video',
|
||||
name: 'pages.VideoPage',
|
||||
component: () => import('@/pages/VideosPage.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: false,
|
||||
onlyManager: true,
|
||||
onlyAdmin: true,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 1020,
|
||||
|
||||
167
src/services/videoService.js
Normal file
167
src/services/videoService.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Api } from '@/store/Api';
|
||||
|
||||
const BASE_URL = process.env.VITE_MONGODB_HOST;
|
||||
|
||||
export const videoService = {
|
||||
// ============ FOLDER METHODS ============
|
||||
|
||||
async getFolders() {
|
||||
const response = await Api.SendReq('/api/video/folders', 'GET');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createFolder(folderName, parentPath = '') {
|
||||
const response = await Api.SendReq('/api/video/folders', 'POST', {
|
||||
folderName,
|
||||
parentPath,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async renameFolder(folderPath, newName) {
|
||||
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'PUT', {
|
||||
newName,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteFolder(folderPath) {
|
||||
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'DELETE');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ VIDEO METHODS ============
|
||||
|
||||
async getVideos(folder = '') {
|
||||
const response = await Api.SendReq('/api/video/videos', 'GET', { folder });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadVideo(file, folder, onProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('video', file);
|
||||
|
||||
// ✅ Folder come query parameter nell'URL
|
||||
const targetFolder = encodeURIComponent(folder || 'default');
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/upload?folder=${targetFolder}`,
|
||||
'POSTFORMDATA',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
5000,
|
||||
formData,
|
||||
null,
|
||||
{
|
||||
timeout: 600000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total && onProgress) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
onProgress(percentCompleted);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadVideos(files, folder, onProgress) {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => formData.append('videos', file));
|
||||
|
||||
// ✅ Folder come query parameter
|
||||
const targetFolder = encodeURIComponent(folder || 'default');
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/upload-multiple?folder=${targetFolder}`,
|
||||
'POSTFORMDATA',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
5000,
|
||||
formData,
|
||||
null,
|
||||
{
|
||||
timeout: 600000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total && onProgress) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
onProgress(percentCompleted);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async renameVideo(folder, filename, newFilename) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}/rename`,
|
||||
'PUT',
|
||||
{ newFilename }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async moveVideo(folder, filename, destinationFolder) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}/move`,
|
||||
'PUT',
|
||||
{ destinationFolder }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteVideo(folder, filename) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}`,
|
||||
'DELETE'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ UTILITY METHODS ============
|
||||
|
||||
getVideoUrl(videoPath) {
|
||||
return `${BASE_URL}${videoPath}`;
|
||||
},
|
||||
|
||||
getStreamUrl(folder, filename) {
|
||||
return `${BASE_URL}/api/video/stream/${folder}/${filename}`;
|
||||
},
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return 'N/A';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default videoService;
|
||||
@@ -1,23 +1,36 @@
|
||||
import axios, {
|
||||
AxiosInstance, AxiosPromise, AxiosResponse, AxiosInterceptorManager,
|
||||
} from 'axios'
|
||||
import { Api } from '@api'
|
||||
import type * as Types from '@/store/Api/ApiTypes'
|
||||
|
||||
AxiosInstance,
|
||||
AxiosPromise,
|
||||
AxiosResponse,
|
||||
AxiosInterceptorManager,
|
||||
} from 'axios';
|
||||
import { Api } from '@api';
|
||||
import type * as Types from '@/store/Api/ApiTypes';
|
||||
|
||||
// Funzione che smista la richiesta in base al metodo
|
||||
async function sendRequest(url, method, mydata, myformdata = null, responsedata = null, options = null) {
|
||||
async function sendRequest(
|
||||
url: any,
|
||||
method: string,
|
||||
mydata: any,
|
||||
myformdata: any = null,
|
||||
responsedata: any = null,
|
||||
options: any = null
|
||||
) {
|
||||
const actions = {
|
||||
get: () => Api.get(url, mydata, responsedata),
|
||||
post: () => Api.post(url, mydata, responsedata, options),
|
||||
postformdata: () => Api.postFormData(url, myformdata, responsedata),
|
||||
postformdata: () => Api.postFormData(url, myformdata, responsedata, options), // ✅ Aggiunto options
|
||||
delete: () => Api.Delete(url, mydata, responsedata),
|
||||
put: () => Api.put(url, mydata, responsedata),
|
||||
patch: () => Api.patch(url, mydata, responsedata),
|
||||
};
|
||||
|
||||
const key = method.toLowerCase();
|
||||
if (actions[key]) return await actions[key]();
|
||||
|
||||
if (actions[key]) {
|
||||
return await actions[key]();
|
||||
}
|
||||
|
||||
throw new Error(`Metodo non supportato: ${method}`);
|
||||
}
|
||||
|
||||
export default sendRequest
|
||||
export default sendRequest;
|
||||
|
||||
@@ -145,7 +145,17 @@ async function Request(
|
||||
|
||||
// ✅ AGGIUNGI IL TIMEOUT DALLE OPTIONS
|
||||
if (options?.timeout) {
|
||||
config.timeout = options.timeout; // in millisecondi (es. 300000 = 5 minuti)
|
||||
config.timeout = options.timeout;
|
||||
}
|
||||
|
||||
// ✅ AGGIUNGI SUPPORTO PER onUploadProgress
|
||||
if (options?.onUploadProgress) {
|
||||
config.onUploadProgress = options.onUploadProgress;
|
||||
}
|
||||
|
||||
// ✅ AGGIUNGI SUPPORTO PER onDownloadProgress (opzionale)
|
||||
if (options?.onDownloadProgress) {
|
||||
config.onDownloadProgress = options.onDownloadProgress;
|
||||
}
|
||||
|
||||
if (options?.stream) config.responseType = 'stream';
|
||||
@@ -210,7 +220,6 @@ async function Request(
|
||||
},
|
||||
...responsedata,
|
||||
});*/
|
||||
|
||||
} else if (type === 'postFormData') {
|
||||
response = await axiosInstance.post(path, payload, config);
|
||||
} else {
|
||||
@@ -221,11 +230,9 @@ async function Request(
|
||||
// Gestione aggiornamento token se necessario
|
||||
//const setAuthToken = path === '/updatepwd' || path === '/users/login';
|
||||
const setAuthToken = !!x_auth_token;
|
||||
if (
|
||||
response && setAuthToken
|
||||
) {
|
||||
if (response && setAuthToken) {
|
||||
const refreshToken = String(response.headers['x-refrtok'] || '');
|
||||
const browser_random = userStore.getBrowserRandom()
|
||||
const browser_random = userStore.getBrowserRandom();
|
||||
if (!x_auth_token) {
|
||||
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
|
||||
}
|
||||
@@ -237,7 +244,7 @@ async function Request(
|
||||
userStore.setAuth(x_auth_token, refreshToken, browser_random);
|
||||
localStorage.setItem(toolsext.localStorage.token, x_auth_token);
|
||||
localStorage.setItem(toolsext.localStorage.refreshToken, refreshToken);
|
||||
localStorage.setItem(toolsext.localStorage. browser_random, browser_random);
|
||||
localStorage.setItem(toolsext.localStorage.browser_random, browser_random);
|
||||
}
|
||||
|
||||
globalStore.setStateConnection('online');
|
||||
@@ -245,7 +252,7 @@ async function Request(
|
||||
return new Types.AxiosSuccess(response.data, response.status);
|
||||
} catch (error) {
|
||||
// Aggiornamento asincrono dello stato di connessione (setTimeout per dare tempo a eventuali animazioni)
|
||||
console.error('Errore funzione Request', error)
|
||||
console.error('Errore funzione Request', error);
|
||||
setTimeout(() => {
|
||||
if (['get'].includes(type.toLowerCase())) {
|
||||
globalStore.connData.downloading_server =
|
||||
|
||||
@@ -49,11 +49,11 @@ export const Api = {
|
||||
return await Request('post', path, payload, responsedata, options);
|
||||
},
|
||||
|
||||
async postFormData(path: string, payload?: any, responsedata?: any) {
|
||||
const globalStore = useGlobalStore();
|
||||
globalStore.connData.uploading_server = 1;
|
||||
globalStore.connData.downloading_server = 1;
|
||||
return await Request('postFormData', path, payload, responsedata);
|
||||
async postFormData(path: string, payload?: any, responsedata?: any, options?: any) {
|
||||
const globalStore = useGlobalStore();
|
||||
globalStore.connData.downloading_server = 1;
|
||||
globalStore.connData.uploading_server = 1;
|
||||
return await Request('postFormData', path, payload, responsedata, options);
|
||||
},
|
||||
|
||||
async get(path: string, payload?: any, responsedata?: any) {
|
||||
@@ -241,7 +241,7 @@ export const Api = {
|
||||
|
||||
if (res.status === serv_constants.RIS_CODE__HTTP_INVALID_TOKEN) {
|
||||
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
|
||||
userStore.setAuth('', '');
|
||||
userStore.setAuth('', '', '');
|
||||
// throw { code: toolsext.ERR_AUTHENTICATION };
|
||||
throw { status: toolsext.ERR_RETRY_LOGIN };
|
||||
}
|
||||
|
||||
100
src/types/video.types.ts
Normal file
100
src/types/video.types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// ============ INTERFACES ============
|
||||
|
||||
export interface IFolder {
|
||||
name: string;
|
||||
path: string;
|
||||
level?: number;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface IVideo {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName?: string;
|
||||
folder: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mimetype?: string;
|
||||
createdAt: string;
|
||||
modifiedAt?: string;
|
||||
uploadedAt?: string;
|
||||
}
|
||||
|
||||
export interface IFolderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface IUploadQueueItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
status: UploadStatus;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IVideoResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
videos?: IVideo[];
|
||||
folders?: IFolder[];
|
||||
video?: IVideo;
|
||||
folder?: IFolder;
|
||||
currentPath?: string;
|
||||
totalVideos?: number;
|
||||
newPath?: string;
|
||||
};
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IFolderResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
folders?: IFolder[];
|
||||
folder?: IFolder;
|
||||
};
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============ TYPES ============
|
||||
|
||||
export type UploadStatus = 'pending' | 'uploading' | 'complete' | 'error';
|
||||
|
||||
// ============ CONSTANTS ============
|
||||
|
||||
export const UPLOAD_STATUS_CONFIG = {
|
||||
pending: {
|
||||
icon: 'schedule',
|
||||
color: 'grey',
|
||||
label: 'In attesa'
|
||||
},
|
||||
uploading: {
|
||||
icon: 'cloud_upload',
|
||||
color: 'primary',
|
||||
label: 'Caricamento...'
|
||||
},
|
||||
complete: {
|
||||
icon: 'check_circle',
|
||||
color: 'positive',
|
||||
label: 'Completato'
|
||||
},
|
||||
error: {
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
label: 'Errore'
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const ALLOWED_VIDEO_TYPES = [
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
'video/x-msvideo',
|
||||
'video/x-matroska'
|
||||
];
|
||||
|
||||
export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
export const MAX_FILES = 10;
|
||||
@@ -6,7 +6,6 @@
|
||||
"target": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
|
||||
"baseUrl": "./",
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
@@ -88,6 +87,12 @@
|
||||
"@icons": [
|
||||
"src/public/myicons/*"
|
||||
],
|
||||
"@types/*": [
|
||||
"src/types/*"
|
||||
],
|
||||
"@services/*": [
|
||||
"src/services/*"
|
||||
],
|
||||
"@images": [
|
||||
"src/public/images/*"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user