- Sistemato INVITI alla App

- Completamento Profilo
- Registrazione tramite Invito, senza richiedere conferma email.
This commit is contained in:
Surya Paolo
2025-11-18 23:56:08 +01:00
parent fc569192e7
commit 4985e7565d
98 changed files with 2209 additions and 3595720 deletions

281
src/components/CCheckIfIsLogged/CCheckIfIsLogged.scss Executable file → Normal file
View File

@@ -1,3 +1,284 @@
.check-login-wrapper {
padding: 1.5rem;
max-width: 800px;
margin: 0 auto;
}
.login-cards-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.login-card {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-4px);
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.15);
}
}
// Card principale di login
.main-card {
background: linear-gradient(135deg, #6b8e23 0%, #556b2f 100%);
color: white;
text-align: center;
.card-icon-wrapper {
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
.card-icon {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
padding: 1rem;
border-radius: 50%;
color: white;
animation: pulse 2s ease-in-out infinite;
}
}
.card-content {
margin-bottom: 2rem;
.card-title {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 1rem 0;
color: white;
}
.card-description {
font-size: 1.05rem;
line-height: 1.6;
margin: 0;
opacity: 0.95;
}
}
.card-actions {
.login-btn {
padding: 0.75rem 2.5rem;
font-size: 1.1rem;
font-weight: 600;
background: white;
color: #6b8e23;
transition: all 0.3s ease;
&:hover {
transform: scale(1.05);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
}
}
}
// Stile per le card inline (Telegram e Aiuto)
.card-content-inline {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1.5rem;
.telegram-info,
.help-info {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
.telegram-icon,
.help-icon {
flex-shrink: 0;
}
.telegram-text,
.help-text {
display: flex;
flex-direction: column;
gap: 0.25rem;
.telegram-title,
.help-title {
font-weight: 600;
font-size: 1.1rem;
color: #333;
}
.telegram-subtitle,
.help-subtitle {
font-size: 0.9rem;
color: #666;
}
}
}
.telegram-btn,
.help-btn {
flex-shrink: 0;
padding: 0.6rem 1.5rem;
font-weight: 600;
white-space: nowrap;
}
}
// Card Telegram
.telegram-card {
.telegram-icon {
color: #0088cc;
}
.telegram-btn {
background: #0088cc;
&:hover {
background: #006699;
}
}
}
// Card Aiuto
.help-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
.help-icon {
color: #6b8e23;
}
.help-btn {
border: 2px solid #6b8e23;
&:hover {
background: #6b8e23;
color: white !important;
}
}
}
// Animazioni
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
// Responsive
@media (max-width: 768px) {
.check-login-wrapper {
padding: 1rem;
}
.login-cards-container {
gap: 1rem;
}
.login-card {
padding: 1.5rem;
}
.main-card {
.card-content {
.card-title {
font-size: 1.5rem;
}
.card-description {
font-size: 1rem;
}
}
.card-actions {
.login-btn {
width: 100%;
padding: 0.75rem 1.5rem;
}
}
}
.card-content-inline {
flex-direction: column;
text-align: center;
.telegram-info,
.help-info {
flex-direction: column;
text-align: center;
}
.telegram-btn,
.help-btn {
width: 100%;
}
}
}
@media (max-width: 480px) {
.main-card {
.card-content {
.card-title {
font-size: 1.25rem;
}
.card-description {
font-size: 0.95rem;
}
}
}
.card-content-inline {
.telegram-text,
.help-text {
.telegram-title,
.help-title {
font-size: 1rem;
}
.telegram-subtitle,
.help-subtitle {
font-size: 0.85rem;
}
}
}
}
// Dark mode support
body.body--dark {
.login-card {
background: rgba(30, 30, 30, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card-content-inline {
.telegram-text,
.help-text {
.telegram-title,
.help-title {
color: #fff;
}
.telegram-subtitle,
.help-subtitle {
color: #aaa;
}
}
}
.help-card {
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
}
}
#logoimg {
height: 300px;
width: auto;

146
src/components/CCheckIfIsLogged/CCheckIfIsLogged.vue Executable file → Normal file
View File

@@ -1,87 +1,91 @@
<template>
<div v-if="showalways || (!showalways && !isLogged)">
<div class="q-pa-md q-gutter-sm">
<div class=" text-center">
<transition
name="fade"
mode="out-in"
appear
enter-active-class="animazione fadeIn"
leave-active-class="animazione fadeOut"
>
<q-banner
:key="1"
rounded
class="text-white bg-red"
color="primary q-title"
style="text-align: center"
>
<template v-slot:avatar>
<q-icon :key="2" name="fas fa-sign-in-alt" color="white" />
</template>
<div :key="3">
<div v-if="showalways || (!showalways && !isLogged)" class="check-login-wrapper">
<transition
name="fade"
mode="out-in"
appear
enter-active-class="animazione fadeIn"
leave-active-class="animazione fadeOut"
>
<div class="login-cards-container">
<!-- Card principale di login -->
<div class="login-card main-card">
<div class="card-icon-wrapper">
<q-icon name="fas fa-sign-in-alt" size="48px" class="card-icon" />
</div>
<div class="card-content">
<h3 class="card-title">Benvenuto su RISO</h3>
<p class="card-description">
Accedi con le tue credenziali per utilizzare la APP e per unirti
al Circuito di scambio RIS del tuo territorio
</div>
<template v-slot:action>
<div>
<q-btn
flat
color="white"
@click="tools.openrighttoolbar()"
>{{ t('login.enter') }}
</q-btn>
</div>
<!--<CRegistration />-->
</template>
</q-banner>
</transition>
<q-separator inset />
<br>
<q-banner
v-if="false"
rounded
dense
size="lg"
class="shadow-5"
color="primary q-title"
style="text-align: center"
>
<div class="mybanner" :key="3">
👉🏻 Entra nel canale Telegram per unirti al gruppo Provinciale:
</p>
</div>
<template v-slot:action>
<div class="card-actions">
<q-btn
type="a"
unelevated
rounded
size="lg"
color="primary"
class="login-btn"
@click="tools.openrighttoolbar()"
>
<q-icon name="fas fa-sign-in-alt" class="q-mr-sm" />
{{ t('login.enter') }}
</q-btn>
</div>
</div>
<!-- Card Telegram (opzionale) -->
<div v-if="false" class="login-card telegram-card">
<div class="card-content-inline">
<div class="telegram-info">
<q-icon name="fab fa-telegram" size="32px" class="telegram-icon" />
<div class="telegram-text">
<span class="telegram-title">Entra nel canale Telegram</span>
<span class="telegram-subtitle">Unisciti al gruppo Provinciale</span>
</div>
</div>
<q-btn
rounded
unelevated
icon="fab fa-telegram"
color="primary"
label="Apri canale"
href="https://t.me/riso_canale/3"
target="_blank"
label="Progetto RISO"
>
</q-btn>
</template>
</q-banner>
</div>
<br />
<q-banner rounded class="bg-green-8 text-white">
<div class="text-h6 text-center">
Visualizza la pagina di Aiuto<br />
<div class="text-center">
<q-btn
:color="$q.dark.isActive ? `black` : `white`"
push
glossy
:text-color="$q.dark.isActive ? `white` : `black`"
label="Pagina Aiuto"
to="/istruzioni"
class="telegram-btn"
/>
</div>
</div>
</q-banner>
</div>
<!-- Card Aiuto -->
<div class="login-card help-card">
<div class="card-content-inline">
<div class="help-info">
<q-icon name="fas fa-question-circle" size="32px" class="help-icon" />
<div class="help-text">
<span class="help-title">Hai bisogno di aiuto?</span>
<span class="help-subtitle">Consulta la guida completa</span>
</div>
</div>
<q-btn
rounded
unelevated
icon="fas fa-book-open"
color="white"
text-color="primary"
label="Vai alla guida"
to="/istruzioni"
class="help-btn"
/>
</div>
</div>
</div>
</transition>
</div>
</template>

View File

@@ -1,5 +1,115 @@
.myflex{
.myflex {
display: flex;
flex: 1;
}
.regulation-container {
:deep(.regulation-content) {
max-width: 900px;
margin: 0 auto;
.reg-header {
text-align: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 2px solid #6b8e23;
h1 {
font-size: 2rem;
font-weight: 700;
color: #6b8e23;
margin: 0;
}
}
.reg-section {
margin-bottom: 2.5rem;
.section-title {
font-size: 1.5rem;
font-weight: 600;
color: #556b2f;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid rgba(107, 142, 35, 0.3);
}
.section-content {
font-size: 1rem;
line-height: 1.7;
color: #333;
text-align: justify;
margin-bottom: 1rem;
strong {
color: #6b8e23;
font-weight: 600;
}
}
.section-list {
list-style: none;
padding-left: 0;
scssli {
padding: 0.75rem 0 0.75rem 3rem; // aumenta da 2rem a 3rem
position: relative;
line-height: 1.6;
&:before {
content: "";
position: absolute;
left: 1rem; // aumenta da 0.5rem a 1rem
color: #6b8e23;
font-size: 1.5rem;
line-height: 1.6;
}
}
}
.highlight-box {
background: rgba(107, 142, 35, 0.08);
border-left: 4px solid #6b8e23;
padding: 1rem 1.5rem;
margin: 1rem 0;
border-radius: 4px;
strong {
display: block;
margin-bottom: 0.5rem;
}
}
}
.reg-footer {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid rgba(107, 142, 35, 0.3);
font-size: 0.95rem;
color: #666;
}
}
}
// Responsive
@media (max-width: 768px) {
.regulation-container :deep(.regulation-content) {
.reg-header h1 {
font-size: 1.5rem;
}
.reg-section {
.section-title {
font-size: 1.25rem;
}
.section-content {
text-align: left;
}
.section-list li {
padding-left: 1.5rem;
}
}
}
}

View File

@@ -1,21 +1,21 @@
import type { PropType } from 'vue';
import { computed, defineComponent, onMounted, ref, watch } from 'vue'
import { useUserStore } from '@store/UserStore'
import { useCircuitStore } from '@store/CircuitStore'
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
import { useUserStore } from '@store/UserStore';
import { useCircuitStore } from '@store/CircuitStore';
import type { ICircuit, IMyCircuit, IMyGroup } from 'model';
import { IImgGallery, IUserFields, IUserProfile, IFriends, IAccount } from 'model'
import { costanti } from '@costanti'
import { shared_consts } from '@src/common/shared_vuejs'
import { tools } from '@tools'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { CUserNonVerif } from '@src/components/CUserNonVerif'
import { CSaldo } from '@src/components/CSaldo'
import { CTitleBanner } from '@src/components/CTitleBanner'
import { toolsext } from '@store/Modules/toolsext'
import { useGlobalStore } from '@store/globalStore'
import { IImgGallery, IUserFields, IUserProfile, IFriends, IAccount } from 'model';
import { costanti } from '@costanti';
import { shared_consts } from '@src/common/shared_vuejs';
import { tools } from '@tools';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { CUserNonVerif } from '@src/components/CUserNonVerif';
import { CSaldo } from '@src/components/CSaldo';
import { CTitleBanner } from '@src/components/CTitleBanner';
import { toolsext } from '@store/Modules/toolsext';
import { useGlobalStore } from '@store/globalStore';
import { userPanel } from 'app/src/rootgen/admin/userPanel';
export default defineComponent({
@@ -56,101 +56,237 @@ export default defineComponent({
type: Object as PropType<IUserFields | null>,
required: false,
default: null,
}
},
},
setup(props, { emit }) {
const userStore = useUserStore();
const circuitStore = useCircuitStore();
const $q = useQuasar();
const { t } = useI18n();
const $router = useRouter();
const $route = useRoute();
const userStore = useUserStore()
const circuitStore = useCircuitStore()
const $q = useQuasar()
const { t } = useI18n()
const $router = useRouter()
const $route = useRoute()
const globalStore = useGlobalStore();
const globalStore = useGlobalStore()
const circuit = ref(<IMyCircuit | ICircuit | null>null);
const account = computed(() =>
circuit.value ? userStore.getAccountByCircuitId(circuit.value._id) : null
);
const circuit = ref(<IMyCircuit | ICircuit | null>null)
const account = computed(() => circuit.value ? userStore.getAccountByCircuitId(circuit.value._id) : null)
const qtarem = computed(() =>
account.value ? circuitStore.getRemainingCoinsToSend(account.value) : 0
);
const saldo_pend = computed(() => (account.value ? account.value.saldo_pend : 0));
const saldo = computed(() => (account.value ? account.value.saldo : 0));
const qtarem = computed(() => account.value ? circuitStore.getRemainingCoinsToSend(account.value) : 0)
const saldo_pend = computed(() => account.value ? account.value.saldo_pend : 0)
const saldo = computed(() => account.value ? account.value.saldo : 0)
const fidoConcessoUtente = computed(() =>
circuitStore.getFidoConcessoByUsername(
props.myuser,
circuit.value._id,
props.username
)
);
const fidoConcessoUtente = computed(() => circuitStore.getFidoConcessoByUsername(props.myuser, circuit.value._id, props.username))
const table = ref(shared_consts.TABLES_CIRCUITS);
const table = ref(shared_consts.TABLES_CIRCUITS)
const showingtooltip = ref(false);
const showrules = ref(false);
const requestToEnterCircuit = ref(false);
const groupnameSel = ref(<IMyGroup | null>null);
const showingtooltip = ref(false)
const showrules = ref(false)
const requestToEnterCircuit = ref(false)
const groupnameSel = ref(<IMyGroup | null>null)
watch(() => props.mycircuit, (newval, oldval) => {
mounted()
})
watch(
() => props.mycircuit,
(newval, oldval) => {
mounted();
}
);
function getNameCircuit() {
if (circuit.value) {
if (tools.existProp(circuit.value, 'name')) {
return tools.getProp(circuit.value, 'name')
return tools.getProp(circuit.value, 'name');
} else if (tools.existProp(circuit.value, 'circuitname')) {
return tools.getProp(circuit.value, 'circuitname')
return tools.getProp(circuit.value, 'circuitname');
}
}
return ''
return '';
}
function mounted() {
groupnameSel.value = props.prop_groupnameSel
groupnameSel.value = props.prop_groupnameSel;
if (!props.mycircuit) {
if (props.circuitname) {
circuit.value = null
circuit.value = null;
}
} else {
if (props.mycircuit) {
circuit.value = props.mycircuit
circuit.value = props.mycircuit;
}
}
if (circuit.value) {
const rectofind = circuitStore.listcircuits.find((circ: ICircuit) => circ.name === getNameCircuit())
const rectofind = circuitStore.listcircuits.find(
(circ: ICircuit) => circ.name === getNameCircuit()
);
if (rectofind) {
// console.log('rectofind', rectofind)
circuit.value = rectofind
circuit.value = rectofind;
}
}
}
function getImgCircuit(circuit: ICircuit) {
return userStore.getImgByCircuit(circuit)
return userStore.getImgByCircuit(circuit);
}
function naviga(path: string) {
$router.push(path)
$router.push(path);
}
function setCmd(cmd: number, myusername: string, value: any = '') {
emit('setCmd', cmd, myusername, value)
emit('setCmd', cmd, myusername, value);
}
function myusername() {
return userStore.my.username
return userStore.my.username;
}
function getRegulation(reg: string) {
const strreg = reg + ''
let name = 'Provinciale';
if (!reg) {
let name = getNameCircuit()
const mystringa = t('circuit.regolamento', { nomecircuito: name })
return mystringa
} else {
return reg
name = getNameCircuit();
}
// Trasforma il vecchio HTML in uno moderno
return transformRegulationHTML(name);
}
onMounted(mounted)
function transformRegulationHTML(circuitName: string): string {
return `
<div class="regulation-content">
<div class="reg-header">
<h1>${circuitName}</h1>
</div>
<div class="reg-section">
<h2 class="section-title">Costituzione e scopo Comunitario</h2>
<p class="section-content">
La Comunità Territoriale "RIS ${circuitName}" spontanea (da ora "la Comunità") costituisce un circuito di scambio
tra i partecipanti, ciascuno dei quali dovrà indicare i beni e servizi che offre alla Comunità stessa in RIS.
Il circuito funziona come dettagliato qui di seguito.
</p>
</div>
<div class="reg-section">
<h2 class="section-title">Circuito di scambio</h2>
<p class="section-content">
La Comunità ha avviato un sistema di scambio di beni e servizi tra utenti, in cui il valore delle transazioni
si misura utilizzando una unità di conto denominata <strong>RIS</strong>, convenzionalmente considerata pari a 1 euro.
</p>
<p class="section-content">
Gli scambi tra utenti avvengono liberamente, dopo aver concordato il valore tra le parti.
</p>
<p class="section-content">
Tutti gli utenti partecipanti al circuito iniziano con un conto vuoto, cioè pari a 0 RIS.
</p>
<p class="section-content">
L'utente pagante che ha un conto RIS vuoto o con quantità non sufficiente a perfezionare lo scambio può utilizzare
la Fiducia Concessa andando in debito fino al limite massimo a lui permesso, denominato "fiducia concessa".
Un equivalente valore in RIS a credito sarà conseguentemente iscritto sul conto del ricevente.
</p>
<p class="section-content">
In ogni momento nel circuito la somma di tutte le esposizioni in positivo saranno perfettamente bilanciate dalla
somma di tutte le esposizioni in negativo, dimostrando così che ciascun detentore di un saldo positivo abbia
garantita la solvibilità del proprio credito. Ciascun detentore di un saldo negativo concorda che la sua
esposizione funge da garanzia della quantità di RIS equivalenti circolanti nel circuito e che può essere chiamato
a regolarizzare con equivalente pagamento in euro *, entro un lasso di tempo ragionevolmente adeguato, nel caso in cui:
</p>
<ul class="section-list">
<li>la Comunità, in accordo con il Gruppo Tecnico, deliberi il rientro di alcune posizioni con decisione motivata;</li>
<li>l'utente decida di uscire per motivi personali;</li>
<li>il circuito chiuda, per decisione deliberata o per motivi di forza maggiore.</li>
</ul>
<div class="highlight-box">
<strong>* Nota importante:</strong>
La regolarizzazione può avvenire con equivalente valore in beni e servizi, oppure, in ultima istanza,
con pagamento in euro.
</div>
</div>
<div class="reg-section">
<h2 class="section-title">Il Gruppo Tecnico</h2>
<p class="section-content">
La Comunità costituisce il Gruppo Tecnico di Gestione degli Scambi (da ora "Gruppo Tecnico"), partecipante esso
stesso agli scambi tramite uno o più delegati autorizzati. Il Gruppo Tecnico è l'organo che supporta la comunità
nella gestione degli scambi e quant'altro sia necessario all'ottimale svolgimento dello scopo comunitario.
</p>
<p class="section-content"><strong>In particolare ha il compito di:</strong></p>
<ul class="section-list">
<li>approvare le richieste di adesione, dopo aver verificato che il richiedente faccia parte della Comunità;</li>
<li>tenere aggiornato l'elenco dei partecipanti al circuito;</li>
<li>stabilire la soglia di massimo scoperto e massimo attivo, collettivo e individuale;</li>
<li>valutare la sostenibilità di eventuali stanziamenti di finanziamenti;</li>
<li>introdurre tassazioni sulle transazioni o una tantum;</li>
<li>adottare o meno il Deperimento dei conti inoperosi (riferimento sistema Si.Cre.Na.C.C.) e modularne i parametri di applicazione;</li>
<li>deliberare la chiusura del circuito;</li>
<li>deliberare la destinazione delle riserve comunitarie depositate sui Conti della Comunità per: finanziare opere varie;
corrispondere salari o agire con interventi di sussistenza a partecipanti in stato di necessità; rilevare lo stato
debitorio all'interno del circuito di utenti in difficoltà economica, irreperibili o venuti a mancare; attuare
interventi ritenuti utili, necessari o meritevoli;</li>
<li>redigere, emendare ed adeguare alle eventuali necessità il presente Regolamento.</li>
</ul>
<p class="section-content">
Il Gruppo Tecnico è formato da candidati scelti dalla Comunità sulla base delle competenze e dovrà sottoporre
all'approvazione della Comunità stessa resoconti periodici delle sue attività.
</p>
</div>
<div class="reg-section">
<h2 class="section-title">Sistemi di contabilizzazione</h2>
<p class="section-content">
I conti RIS e le transazioni conseguenti agli scambi sono registrati attraverso la piattaforma <strong>riso.app</strong>,
dove i partecipanti comunicano i beni e servizi proposti alla Comunità. Tutti i partecipanti possono visualizzare
in ogni momento la situazione del circuito.
</p>
<p class="section-content">
La Comunità, in accordo con il Gruppo Tecnico, può decidere di utilizzare altri strumenti di contabilizzazione,
che dovranno, in ogni caso, permettere l'inserimento delle singole transazioni effettuate tra gli utenti e la
visualizzazione da parte di ogni partecipante. Saranno dunque inseriti nel nuovo strumento tutti gli utenti
partecipanti con l'ultimo saldo rilevato al momento della transizione.
</p>
</div>
<div class="reg-section">
<h2 class="section-title">Conto Comunitario</h2>
<p class="section-content">
La Comunità può decidere di aprire uno o più Conti all'interno del circuito, che saranno gestiti dal Gruppo Tecnico,
allo scopo di interagire con i conti degli altri utenti e quindi recepire eventuali proventi da tassazioni o
finanziare iniziative autorizzate. I Conti Comunitari sono i conti della Comunità.
</p>
<p class="section-content">
Un conto a credito rappresenta una riserva monetaria da re-immettere nel circuito per finanziare opere o attività
di beneficio comune; un conto a debito rappresenta il "debito pubblico" della Comunità.
</p>
<p class="section-content">
I conti comunitari sono sottoposti alle stesse regole degli altri conti (conto vuoto, possibilità di scoperto di conto, ecc.).
Ogni operazione straordinaria viene deliberata appositamente, mentre l'ordinaria amministrazione può essere autorizzata
in via continuativa.
</p>
</div>
<div class="reg-section">
<h2 class="section-title">Riferimenti</h2>
<p class="section-content">
Per una migliore comprensione dei meccanismi che sottostanno allo scambio nel qui presentato circuito, si rimanda
alla lettura del testo disponibile gratuitamente su <strong>https://sicrenacc.info</strong>, da cui è stata tratta
libera ispirazione e condivisione dei principi generali del Sistema di Credito Naturale.
</p>
</div>
</div>
`;
}
onMounted(mounted);
return {
circuit,
@@ -176,6 +312,6 @@ export default defineComponent({
requestToEnterCircuit,
groupnameSel,
fidoConcessoUtente,
}
};
},
})
});

View File

@@ -564,12 +564,14 @@
></q-btn>
<div
v-if="showrules"
v-html="getRegulation(circuit.regulation)"
></div>
class="regulation-container q-mb-lg"
>
<div v-html="getRegulation(circuit.regulation)"></div>
</div>
</q-card-section>
<q-card-actions align="center">
<q-btn
class="centeritems"
class="centeritems q-ma-lg"
icon="fas fa-user-plus"
color="positive"
:label="$t('circuit.acceptregulation')"

View File

@@ -1201,7 +1201,7 @@
>
</CShareSocial>
</q-dialog>
<q-dialog v-model="mostraInviti">
<q-dialog v-model="mostraInviti" maximized>
<q-card style="min-width: 350px; max-width: 600px">
<!-- Header con bottone chiudi -->
<q-bar class="bg-primary text-white">

View File

@@ -223,7 +223,7 @@ export default defineComponent({
}
if (
mypathin.value === 'home_logout' &&
(mypathin.value === 'home_logout' || mypathin.value === 'presentazione') &&
globalStore.site.name === 'local' &&
!rec.value
) {

View File

@@ -111,7 +111,7 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
font-weight: 500;
@media (max-width: 600px) {
font-size: 13px;
font-size: 16px;
}
}
@@ -247,7 +247,7 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
line-height: 1.3;
@media (max-width: 600px) {
font-size: 15px;
font-size: 16px;
}
.step-completed & {
@@ -262,7 +262,7 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
margin-top: 2px;
@media (max-width: 600px) {
font-size: 12px;
font-size: 15px;
}
}
@@ -280,12 +280,12 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.step-description {
margin: 0 0 16px 0;
font-size: 14px;
font-size: 16px;
line-height: 1.6;
color: #475569;
@media (max-width: 600px) {
font-size: 13px;
font-size: 15px;
margin: 0 0 12px 0;
}
}
@@ -305,23 +305,23 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 100%;
height: 44px;
font-weight: 600;
font-size: 15px;
font-size: 16px;
text-transform: none;
letter-spacing: 0.3px;
@media (max-width: 600px) {
height: 42px;
font-size: 14px;
font-size: 16px;
}
}
.skip-btn {
align-self: center;
font-size: 13px;
font-size: 16px;
text-transform: none;
@media (max-width: 600px) {
font-size: 12px;
font-size: 15px;
}
}
@@ -330,12 +330,12 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: rgba(33, 186, 69, 0.1);
border-left: 4px solid $success-color;
border-radius: 8px;
font-size: 14px;
font-size: 16px;
line-height: 1.6;
color: #0d5c2a;
@media (max-width: 600px) {
font-size: 13px;
font-size: 15px;
padding: 10px 14px;
}
}
@@ -358,7 +358,7 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
:deep(.q-field__label) {
font-weight: 500;
font-size: 14px;
font-size: 16px;
}
}
@@ -397,12 +397,12 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.completion-text {
font-size: 16px;
font-size: 18px;
font-weight: 600;
color: $success-color;
@media (max-width: 600px) {
font-size: 15px;
font-size: 16px;
}
}
@@ -446,13 +446,13 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px;
transition: $transition;
flex-shrink: 0;
font-size: 14px;
font-size: 16px;
min-width: 120px;
@media (max-width: 600px) {
height: 42px;
min-width: 100px;
font-size: 13px;
font-size: 15px;
}
@media (max-width: 400px) {
@@ -510,17 +510,17 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
:deep(.q-banner__content) {
font-size: 14px;
font-size: 16px;
line-height: 1.6;
@media (max-width: 600px) {
font-size: 13px;
font-size: 15px;
}
}
:deep(.q-btn) {
@media (max-width: 600px) {
font-size: 13px;
font-size: 15px;
padding: 8px 12px;
}
}
@@ -564,13 +564,13 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
:deep(.q-badge) {
padding: 4px 10px;
font-size: 12px;
font-size: 14px;
font-weight: 600;
border-radius: 12px;
@media (max-width: 600px) {
padding: 3px 8px;
font-size: 11px;
font-size: 12px;
}
}

View File

@@ -189,7 +189,7 @@ export default defineComponent({
const stepCircuitItalia = computed(() => ({
step: STEP_CIRCUIT_ITALIA,
title: t('tutorial.step_circuito_italia_title') || 'Circuito Italia',
title: t('tutorial.step_circuito_italia_title') || 'Circuito RIS Italia',
extratitle: function () {
return circuititalia.value ? ': ' + circuititalia.value.name : '';
},
@@ -234,7 +234,7 @@ export default defineComponent({
// Step 3: Circuito Locale (solo se disponibile)
steps.push({
key: 'circuit',
name: 'Circuito Locale',
name: 'Circuito RIS Locale',
completed: stepCircuit.value.checkOk(),
step: STEP_CIRCUIT,
});
@@ -242,7 +242,7 @@ export default defineComponent({
// Step 4: Circuito Italia (solo se circuito locale completato)
steps.push({
key: 'circuitItalia',
name: 'Circuito Italia',
name: 'Circuito RIS Italia',
completed: stepCircuitItalia.value.checkOk(),
step: STEP_CIRCUIT_ITALIA,
});
@@ -272,9 +272,9 @@ export default defineComponent({
},
{
key: 'circuit',
visible: !!mycircuit.value,
visible: true,
disabled: false,
title: 'Circuito Locale',
title: 'Circuito RIS Locale',
description: 'Seleziona la tua provincia di residenza per connetterti con la community locale.',
completed: stepCircuit.value.checkOk(),
avatar: {
@@ -308,8 +308,8 @@ export default defineComponent({
? 'Completato!'
: 'Unisciti al circuito nazionale (opzionale)',
badge: {
color: stepCircuitItalia.value.checkOkReal() ? 'positive' : (isSalta(STEP_CIRCUIT_ITALIA) || isSalta(STEP_CIRCUIT)) ? 'red' : 'grey',
label: stepCircuitItalia.value.checkOkReal() ? 'Fatto' : (isSalta(STEP_CIRCUIT_ITALIA) || isSalta(STEP_CIRCUIT)) ? 'Saltato' : 'opzionale',
color: stepCircuitItalia.value.checkOkReal() ? 'positive' : (isSalta(STEP_CIRCUIT_ITALIA)) ? 'red' : 'grey',
label: stepCircuitItalia.value.checkOkReal() ? 'Fatto' : (isSalta(STEP_CIRCUIT_ITALIA)) ? 'Saltato' : 'opzionale',
},
},
]);
@@ -354,7 +354,8 @@ export default defineComponent({
const totalSteps = computed(() => {
let count = 0;
count++; // Telegram
if (mycircuit.value) count++; // Circuito Locale
count++; // Circuito Locale
// if (mycircuit.value)
if (circuititalia.value) count++; // Circuito Italia
return count;
});
@@ -571,8 +572,8 @@ export default defineComponent({
function isSalta(step: number) {
return (
(step === STEP_CIRCUIT && mycircuit.value && isAskedToCircuit()) ||
(step === STEP_CIRCUIT_ITALIA && circuititalia.value && isAskedToCircuitItalia())
(step === STEP_CIRCUIT && mycircuit.value && userStore.my.profile.noCircuit) ||
(step === STEP_CIRCUIT_ITALIA && circuititalia.value && userStore.my.profile.noCircIta)
);
}

View File

@@ -1,46 +0,0 @@
.signup {
width: 100%;
margin: 0 auto;
max-width: 450px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.clCellCode {
border-radius: 32px;
border-right: #2d2260;
height: 50px;
font-size: 1rem;
padding: 8px;
}
.clCell {
border-radius: 32px;
border-right: #2d2260;
height: 50px;
font-size: 1rem;
padding: 8px;
}
.vue-country-select{
border-radius: 32px;
}
.myuserinvitante{
font-weight: bold;
color: red;
font-size: 1.5rem;
}
.cosa_chiedere{
font-weight: bold;
color: blue;
font-size: 1rem;
padding: 10px;
}

View File

@@ -1,461 +0,0 @@
import { tools } from '@tools'
import type { ISignupOptions } from 'model'
import { Logo } from '@src/components/logo'
// import 'vue-country-code/dist/vue-country-code.css'
import { CTitleBanner } from '../CTitleBanner'
import { CCopyBtn } from '../CCopyBtn'
import { CRegistration } from '../CRegistration'
import { PagePolicy } from '../PagePolicy'
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
import { CSignIn } from '@src/components/CSignIn'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { DefaultProfile, useUserStore } from '@store/UserStore'
import useValidate from '@vuelidate/core'
import useVuelidate from '@vuelidate/core'
import { shared_consts } from '@src/common/shared_vuejs'
import { minLength, required, sameAs } from '@vuelidate/validators'
// import { ValidationRuleset } from 'vuelidate'
import { complexity, complexityUser, registereduser, aportadorexist } from '../../validation'
// import 'vue3-tel-input/dist/vue3-tel-input.css'
import { useRoute, useRouter } from 'vue-router'
import { static_data } from '@src/db/static_data'
import { useGlobalStore } from '@store/globalStore'
// import {Loading, QSpinnerFacebook, QSpinnerGears} from 'quasar'
export default defineComponent({
name: 'CSignUp',
components: { Logo, CTitleBanner, PagePolicy, CCopyBtn, CRegistration },
props: {
showadultcheck: {
type: Boolean,
required: false,
default: false,
},
showcell: {
type: Boolean,
required: false,
default: false,
},
showaportador: {
type: Boolean,
required: false,
default: false,
},
shownationality: {
type: Boolean,
required: false,
default: false,
},
show_namesurname: {
type: Boolean,
required: false,
default: true,
},
regexpire: {
type: String,
required: false,
default: '',
},
name_default: {
type: String,
required: false,
default: '',
},
username_default: {
type: String,
required: false,
default: '',
},
need_Telegram: {
type: Boolean,
required: false,
default: false,
},
collettivo: {
type: Boolean,
required: false,
default: false,
},
token: {
type: String,
required: false,
default: '',
},
},
setup(props, { emit }) {
const $q = useQuasar()
const { t } = useI18n()
const userStore = useUserStore()
const $route = useRoute()
const $router = useRouter()
const countryname = ref('')
const iamadult = ref(false)
const duplicate_email = ref(false)
const duplicate_username = ref(false)
const visureg = ref(false)
const showpolicy = ref(false)
const visubuttBOT = ref(false)
const isalreadyReg = ref(false)
const needTelegram = ref(false)
const slide = ref('1')
const inputAportador = ref(<any>null)
const inputEmail = ref(<any>null)
const inputUsername = ref(<any>null)
const inputName = ref(<any>null)
const inputSurname = ref(<any>null)
const inputPassword = ref(<any>null)
const inputPassword2 = ref(<any>null)
const checkifDisabled = computed(() => {
let ret = true
if (slide.value === '1') {
// Invitante + Email
ret = !signup.email || (tools.getAskToVerifyReg() && (!signup.aportador_solidario || inputAportador.value.hasError)) || (inputEmail.value && inputEmail.value.hasError)
} else if (slide.value === '2') {
// Username
ret = !signup.username || (inputUsername.value && inputUsername.value.hasError)
if (tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) {
ret = ret || (!signup.name || (inputName.value && inputName.value.hasError))
ret = ret || (!signup.surname || (inputSurname.value && inputSurname.value.hasError))
}
} else if (slide.value === '3') {
// Password
ret = !signup.password || (!inputPassword.value || (inputPassword.value && inputPassword.value.hasError)) || (!inputPassword2.value || (inputPassword2.value && inputPassword2.value.hasError))
}
return ret
})
const typePassword = ref('password')
const ap_iniziale = ref('')
const globalStore = useGlobalStore()
const site = computed(() => globalStore.site)
const signup = reactive(<ISignupOptions>{
email: '',
username: '',
name: '',
surname: '',
password: '',
repeatPassword: '',
terms: false,
profile: DefaultProfile,
aportador_solidario: '',
})
const validations: any = computed(() => {
const valid: any = {
repeatPassword: {
required,
repeatPassword: sameAs(signup.password),
},
password: {
required,
minLength: minLength(8),
complexity,
},
username: {
required,
minLength: minLength(4),
complexityUser,
registereduser,
},
name: {
required: (props.collettivo || tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) ? true : false,
},
surname: {
required: (tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) ? true : false,
},
terms: {
required,
},
aportador_solidario: {
aportadorexist,
required
}
}
if (props.show_namesurname) {
valid.name = {
}
valid.surname = {
}
}
return valid
})
// @ts-ignore
const v$ = useVuelidate(validations, signup)
const invited = ref($route.params.invited)
const usernameteleg = ref($route.params.usernameteleg)
const idteleg = ref($route.params.idteleg)
watch(() => slide.value, (to: any, from: any) => {
if (slide.value === '3') {
v$.value.$touch()
}
})
watch(() => invited, (to: any, from: any) => {
if (props.showaportador) {
console.log('changeaportador', $route.params.invited)
if (!signup.aportador_solidario) {
if ($route.params.invited) {
// @ts-ignore
signup.aportador_solidario = $route.params.invited
}
}
}
})
function allowSubmit() {
let error = v$.value.$error || v$.value.$invalid || globalStore.serverError
if (props.showadultcheck)
error = error || !iamadult.value
if (props.showcell) {
if (signup.profile)
error = error || signup.profile.cell!.length <= 6
else
error = true
}
if (tools.getAskToVerifyReg()) {
error = error || !signup.aportador_solidario
}
return !error
}
function env() {
return process.env
}
function changeemail() {
signup.email = tools.removespaces(signup.email!)
signup.email = signup.email.toLowerCase()
emit('update:value', signup.email)
}
function changeusername(value: string) {
signup.username = tools.removespaces(signup.username)
emit('update:value', signup.username)
}
function submitOk() {
v$.value.$touch()
signup.email = tools.removespaces(signup.email!)
signup.email = signup.email.toLowerCase()
signup.username = tools.removespaces(signup.username)
// remove @
signup.username = tools.removeAt(signup.username)
duplicate_email.value = false
duplicate_username.value = false
if (!signup.terms) {
tools.showNotif($q, t('reg.err.terms'))
return
}
/*if (v$.signup.$error) {
tools.showNotif($q, t('reg.err.errore_generico'))
return
} */
if (signup.name) {
signup.name = tools.CapitalizeAllWords(signup.name)
signup.surname = tools.CapitalizeAllWords(signup.surname)
}
$q.loading.show({ message: t('reg.incorso') })
console.log(signup)
return userStore.signup(tools.clone(signup))
.then((ris: any) => {
if (tools.SignUpcheckErrors($q, $router, ris.code, ris.msg))
$q.loading.hide()
}).catch((error: string) => {
console.log('ERROR = ' + error)
$q.loading.hide()
})
}
function intcode_change(coderec: any) {
// console.log('intcode', coderec)
if (signup.profile) {
signup.profile.intcode_cell = '+' + coderec.dialCode
signup.profile.iso2_cell = coderec.iso2
}
}
function selectcountry({ name, iso2, dialCode }: { name: string, iso2: string, dialCode: string }) {
// console.log(name, iso2, dialCode)
signup.profile.nationality = iso2
countryname.value = name
}
async function created() {
needTelegram.value = props.need_Telegram
console.log('$route.params', $route.params)
ap_iniziale.value = $route.params.invited ? $route.params.invited.toString() : ''
signup.aportador_solidario = $route.params.invited ? $route.params.invited.toString() : ''
signup.username = $route.params.usernameteleg ? $route.params.usernameteleg.toString() : ''
signup.regexpire = $route.params.regexpire ? $route.params.regexpire.toString() : props.regexpire
if (signup.username)
isalreadyReg.value = await tools.registeredusername(signup.username)
signup.profile.username_telegram = signup.username
if ($route.params.idteleg) {
signup.profile.teleg_id = $route.params.idteleg ? parseInt($route.params.idteleg.toString(), 10) : 0
}
if (props.collettivo) {
signup.username = props.username_default!
signup.name = props.name_default!
}
// console.log('1) aportador_solidario', signup.aportador_solidario)
if (!signup.aportador_solidario)
signup.aportador_solidario = tools.getCookie(tools.APORTADOR_SOLIDARIO, signup.aportador_solidario)
if (!signup.aportador_solidario || signup.aportador_solidario === 'undefined') {
if (!tools.getAskToVerifyReg()) {
signup.aportador_solidario = tools.APORTADOR_NONE
}
}
// console.log('signup.aportador_solidario', signup.aportador_solidario)
// console.log('getasktoverify', tools.getAskToVerifyReg())
if (tools.getAskToVerifyReg()) {
if (!signup.username || !signup.profile.teleg_id) {
// tools.copyStringToClipboard($q, signup.aportador_solidario, true)
visubuttBOT.value = true
// window.location.href = tools.getLinkBotTelegram()
}
}
}
function myRuleEmail(val: string) {
return new Promise((resolve, reject) => {
// call
// resolve(true)
// --> content is valid
// resolve(false)
// --> content is NOT valid, no error message
// resolve(error_message)
// --> content is NOT valid, we have error message
tools.registeredemail(val).then((emailOk) => {
let risp = !!emailOk || t('reg.err.duplicate_email')
if (emailOk) {
risp = tools.isEmail(val) || t('reg.err.invalid_email')
emailOk = emailOk && tools.isEmail(val)
}
if (emailOk) {
// risp = !tools.isEmailNoMicroZozz(val) || t('reg.err.invalid_email_micro')
}
resolve(risp)
})
// calling reject(...) will also mark the input
// as having an error, but there will not be any
// error message displayed below the input
// (only in browser console)
})
}
function showPassword() {
//
typePassword.value = typePassword.value === 'password' ? 'text' : 'password'
}
function regEventEmail(invited: boolean) {
console.log('EVENT RECEIVED: regEventEmail', invited)
// reg
visubuttBOT.value = false
needTelegram.value = false
}
onMounted(() => {
const token = props.token
// carica
})
created()
return {
changeemail,
changeusername,
submitOk,
selectcountry,
intcode_change,
tools,
countryname,
signup,
iamadult,
v$,
t,
allowSubmit,
myRuleEmail,
visureg,
showpolicy,
visubuttBOT,
isalreadyReg,
site,
showPassword,
typePassword,
ap_iniziale,
regEventEmail,
needTelegram,
slide,
checkifDisabled,
inputAportador,
inputEmail,
inputUsername,
inputName,
inputSurname,
inputPassword,
inputPassword2,
shared_consts,
}
},
})

View File

@@ -1,739 +0,0 @@
<template>
<div>
<div
v-if="tools.isLogged() && tools.getUsername() && !collettivo"
class="text-center"
>
<q-banner rounded class="bg-green text-white" style="text-align: center">
<span class="mybanner">
{{ tools.getUsername() }} sei già correttamente registrato ed hai
accesso alla Piattaforma<br />
</span>
</q-banner>
<div class="row q-ma-sm q-pa-sm justify-center">
<q-btn
class="q-ma-sm"
color="primary"
icon="fas fa-home"
label="Vai alla Home"
to="/"
></q-btn>
<q-btn
class="q-ma-sm"
color="accent"
icon="fas fa-sign"
label="Voglio vedere la pagina di Registrazione"
@click="visureg = true"
></q-btn>
<br />
</div>
</div>
<div v-if="!tools.isLogged() || visureg || collettivo" class="text-center">
<div>
<div>
<logo
mystyle="width: 40px !important; height: 40px !important; "
></logo>
<div v-if="!isalreadyReg && !(visubuttBOT && needTelegram)">
<CTitleBanner :title="$t('pages.SignUp')"></CTitleBanner>
</div>
</div>
</div>
<div
v-if="visubuttBOT && needTelegram && !collettivo"
class="q-gutter-md"
>
<div class="q-ma-md">
<CRegistration
:invited="signup.aportador_solidario"
:regexpire="regexpire"
@regEventEmail="regEventEmail"
:signupform="true"
/>
</div>
</div>
<div v-else-if="!isalreadyReg || collettivo" class="q-gutter-sm q-mt-sm">
<div v-if="signup.username === 'undefined'">
<br />
Vai su <b>BOT RISO</b> Telegram ed imposta l'Username di Telegram.<br /><br />
<q-btn
rounded
color="primary"
icon="fab fa-telegram"
label="Apri BOT"
type="a"
:href="
tools.getLinkBotTelegram(signup.aportador_solidario, regexpire)
"
target="_blank"
></q-btn>
<br /><br />
</div>
<div v-else>
<div v-if="signup.terms">
<q-input
v-if="
showaportador &&
signup.aportador_solidario !== tools.APORTADOR_NONE &&
v$.aportador_solidario.$error
"
ref="inputAportador"
bg-color="light-blue-4"
:readonly="!!ap_iniziale"
v-model="signup.aportador_solidario"
rounded
outlined
@keyup.enter="
v$.aportador_solidario.$touch && !v$.aportador_solidario.$error
? $refs.inputEmail.focus()
: null
"
@blur="v$.aportador_solidario.$touch"
:error="v$.aportador_solidario.$error"
:error-message="
tools.errorMsg('aportador_solidario', v$.aportador_solidario)
"
maxlength="20"
debounce="1000"
:label="
collettivo
? t('reg.username_admin_collettivo')
: t('reg.aportador_solidario')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<div style="margin-top: 5px"></div>
<q-input
ref="inputEmail"
v-model="signup.email"
rounded
outlined
@update:model-value="changeemail()"
maxlength="50"
v-on:keyup.enter="!checkifDisabled ? $refs.carousel.next() : null"
debounce="2000"
:rules="[myRuleEmail]"
:label="
collettivo
? t('reg.email_reg_collettivo')
: t('reg.email_reg')
"
>
<template v-slot:prepend>
<q-icon name="email" />
</template>
</q-input>
<q-input
ref="inputUsername"
v-model="signup.username"
:readonly="
tools.getAskToVerifyReg() &&
!site.confpages?.enableRegMultiChoice
"
rounded
outlined
@blur="v$.username.$touch"
@update:model-value="changeusername"
:error="v$.username.$error"
@keydown.space="(event) => event.preventDefault()"
@keyup.enter="
!v$.username.$error ? $refs.inputName.focus() : null
"
maxlength="20"
debounce="500"
:error-message="
tools.errorMsg('username', v$.username) ||
(isalreadyReg ? 'L\'Username è gia stato registrato!' : '')
"
:label="
tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.askUSernameTelegramToTheReg
)
? t('reg.username_telegram')
: t('reg.username_reg')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<div v-if="collettivo">
<q-input
ref="inputName"
v-model="signup.name"
rounded
outlined
@blur="v$.name.$touch"
:error="v$.name.$error"
maxlength="30"
debounce="1000"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
:error-message="tools.errorMsg('name', v$.name)"
:label="$t('reg.name_opt_collettivo')"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
</div>
<div v-else-if="show_namesurname">
<q-input
ref="inputName"
v-model="signup.name"
rounded
outlined
@blur="v$.name.$touch"
:error="v$.name.$error"
maxlength="30"
debounce="1000"
@keyup.enter="$refs.inputSurname.focus()"
:error-message="tools.errorMsg('name', v$.name)"
:label="
tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.regNameSurnameMandatory
)
? t('reg.name')
: t('reg.name_opt')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<q-input
v-if="signup.surname"
ref="inputSurname"
v-model="signup.surname"
rounded
outlined
:error="v$.surname.$error"
@blur="v$.surname.$touch"
maxlength="30"
debounce="1000"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
:error-message="tools.errorMsg('surname', v$.surname)"
:label="$t('reg.surname_opt')"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<q-input
ref="inputPassword"
v-model="signup.password"
:type="typePassword"
rounded
outlined
@blur="v$.password.$touch"
:error="v$.password.$error"
:error-message="`${tools.errorMsg('password', v$.password)}`"
@keyup.enter="
!v$.password.$error ? $refs.inputPassword2.focus() : null
"
maxlength="30"
debounce="1000"
:label="$t('reg.password_reg')"
>
<template v-slot:append>
<q-btn
tabindex="-1"
:icon="
typePassword === `password`
? `fas fa-eye-slash`
: `fas fa-eye`
"
@click="showPassword"
>
</q-btn>
</template>
<template v-slot:prepend>
<q-icon name="vpn_key" />
</template>
</q-input>
</div>
<q-input
ref="inputPassword2"
v-model="signup.repeatPassword"
:type="typePassword"
maxlength="30"
rounded
outlined
@blur="v$.repeatPassword.$touch"
:error="v$.repeatPassword.$error"
:error-message="`${tools.errorMsg(
'repeatpassword',
v$.repeatPassword
)}`"
v-on:keyup.enter="!checkifDisabled ? $refs.carousel.next() : null"
:label="$t('reg.repeatPassword')"
>
<template v-slot:append>
<q-btn
tabindex="-1"
:icon="
typePassword === `password`
? `fas fa-eye-slash`
: `fas fa-eye`
"
@click="showPassword"
>
</q-btn>
</template>
<template v-slot:prepend>
<q-icon name="vpn_key" />
</template>
</q-input>
<div class="column">
<q-btn
rounded
size="lg"
color="positive"
@click="submitOk"
:label="$t('reg.submit')"
>
</q-btn>
<br />
</div>
</div>
<div v-else>
<q-carousel
v-model="slide"
ref="carousel"
transition-prev="slide-right"
transition-next="slide-left"
animated
swipeable
:class="`shadow-1`"
>
<template v-slot:control>
<q-carousel-control
position="bottom-right"
:offset="[18, 18]"
class="q-gutter-xs"
>
<q-btn
v-if="slide !== '1'"
push
text-color="black"
icon="arrow_left"
:label="$t('dialog.indietro')"
@click="$refs.carousel.previous()"
/>
<q-btn
v-if="slide !== '4'"
push
color="primary"
icon="arrow_right"
:label="$t('dialog.avanti')"
:disabled="checkifDisabled"
@click="!checkifDisabled ? $refs.carousel.next() : null"
/>
</q-carousel-control>
</template>
<q-carousel-slide name="1">
<div class="">
<q-input
v-if="
showaportador &&
signup.aportador_solidario !== tools.APORTADOR_NONE
"
ref="inputAportador"
bg-color="light-blue-4"
:readonly="!!ap_iniziale"
v-model="signup.aportador_solidario"
rounded
outlined
@keyup.enter="
v$.aportador_solidario.$touch &&
!v$.aportador_solidario.$error
? $refs.inputEmail.focus()
: null
"
@blur="v$.aportador_solidario.$touch"
:error="v$.aportador_solidario.$error"
:error-message="
tools.errorMsg(
'aportador_solidario',
v$.aportador_solidario
)
"
maxlength="20"
debounce="1000"
:label="
collettivo
? t('reg.username_admin_collettivo')
: t('reg.aportador_solidario')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<div style="margin-top: 5px"></div>
<q-input
ref="inputEmail"
v-model="signup.email"
rounded
outlined
@update:model-value="changeemail()"
maxlength="50"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
debounce="2000"
:rules="[myRuleEmail]"
:label="
collettivo
? t('reg.email_reg_collettivo')
: t('reg.email_reg')
"
>
<template v-slot:prepend>
<q-icon name="email" />
</template>
</q-input>
</div>
</q-carousel-slide>
<q-carousel-slide name="2">
<div class="cosa_chiedere">{{ t('reg.scegli_username') }}</div>
<q-input
ref="inputUsername"
v-model="signup.username"
:readonly="
tools.getAskToVerifyReg() &&
!site.confpages?.enableRegMultiChoice
"
rounded
outlined
@blur="v$.username.$touch"
@update:model-value="changeusername"
:error="v$.username.$error"
@keydown.space="(event) => event.preventDefault()"
@keyup.enter="
!v$.username.$error ? $refs.inputName.focus() : null
"
maxlength="20"
debounce="500"
:error-message="
tools.errorMsg('username', v$.username) ||
(isalreadyReg ? 'L\'Username è gia stato registrato!' : '')
"
:label="
collettivo
? t('reg.username_reg_collettivo')
: tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.askUSernameTelegramToTheReg
)
? t('reg.username_telegram')
: t('reg.username_reg')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<div v-if="collettivo">
<q-input
ref="inputName"
v-model="signup.name"
rounded
outlined
@blur="v$.name.$touch"
:error="v$.name.$error"
maxlength="30"
debounce="1000"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
:error-message="tools.errorMsg('name', v$.name)"
:label="$t('reg.name_opt_collettivo')"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
</div>
<div v-else-if="show_namesurname">
<q-input
ref="inputName"
v-model="signup.name"
rounded
outlined
@blur="v$.name.$touch"
:error="v$.name.$error"
maxlength="30"
debounce="1000"
@keyup.enter="$refs.inputSurname.focus()"
:error-message="tools.errorMsg('name', v$.name)"
:label="
tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.regNameSurnameMandatory
)
? t('reg.name')
: t('reg.name_opt')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
<q-input
ref="inputSurname"
v-model="signup.surname"
rounded
outlined
:error="v$.surname.$error"
@blur="v$.surname.$touch"
maxlength="30"
debounce="1000"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
:error-message="tools.errorMsg('surname', v$.surname)"
:label="
tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.regNameSurnameMandatory
)
? t('reg.surname')
: t('reg.surname_opt')
"
>
<template v-slot:prepend>
<q-icon name="person" />
</template>
</q-input>
</div>
</q-carousel-slide>
<q-carousel-slide name="3">
<div class="cosa_chiedere">{{ t('reg.scegli_password') }}</div>
<q-input
ref="inputPassword"
v-model="signup.password"
class="q-mb-md"
:type="typePassword"
rounded
outlined
@blur="v$.password.$touch"
:error="v$.password.$error"
:error-message="`${tools.errorMsg('password', v$.password)}`"
@keyup.enter="
!v$.password.$error ? $refs.inputPassword2.focus() : null
"
maxlength="30"
debounce="1000"
:label="$t('reg.password_reg')"
>
<template v-slot:append>
<q-btn
tabindex="-1"
:icon="
typePassword === `password`
? `fas fa-eye-slash`
: `fas fa-eye`
"
@click="showPassword"
>
</q-btn>
</template>
<template v-slot:prepend>
<q-icon name="vpn_key" />
</template>
</q-input>
<q-input
ref="inputPassword2"
v-model="signup.repeatPassword"
:type="typePassword"
maxlength="30"
rounded
outlined
@blur="v$.repeatPassword.$touch"
:error="v$.repeatPassword.$error"
:error-message="`${tools.errorMsg(
'repeatpassword',
v$.repeatPassword
)}`"
v-on:keyup.enter="
!checkifDisabled ? $refs.carousel.next() : null
"
:label="$t('reg.repeatPassword')"
>
<template v-slot:append>
<q-btn
tabindex="-1"
:icon="
typePassword === `password`
? `fas fa-eye-slash`
: `fas fa-eye`
"
@click="showPassword"
>
</q-btn>
</template>
<template v-slot:prepend>
<q-icon name="vpn_key" />
</template>
</q-input>
</q-carousel-slide>
<q-carousel-slide name="4">
<q-input
v-if="shownationality"
v-model="countryname"
:readonly="true"
rounded
outlined
debounce="1000"
:label="$t('reg.nationality')"
>
<template v-slot:prepend>
<!--<vue-country-code
@onSelect="selectcountry"
:preferredCountries="tools.getprefCountries"
:dropdownOptions="{ disabledDialCode: true }"
>
</vue-country-code>-->
</template>
</q-input>
<!--<vue-tel-input
v-if="showcell"
@country-changed="intcode_change()"
:value="signup.profile.cell"
:placeholder="$t('reg.cell')"
maxlength="20"
:enabledCountryCode="true"
inputClasses="clCell"
wrapperClasses="clCellCode">
</vue-tel-input>-->
<div class="text-center">
<q-btn
label="Mostra Privacy"
@click="showpolicy = true"
></q-btn>
</div>
<q-dialog v-model="showpolicy">
<q-card class="dialog_card">
<q-toolbar class="bg-primary text-white">
<q-toolbar-title> Privacy Policy </q-toolbar-title>
<q-btn
flat
round
color="white"
icon="close"
v-close-popup
></q-btn>
</q-toolbar>
<q-card-section class="inset-shadow">
<PagePolicy
v-if="site.policy"
:owneremail="site.policy.owneremail"
:siteName="site.policy.siteName"
:ownerDataName="site.policy.ownerDataName"
:managerData="site.policy.managerData"
:includeData="site.policy.includeData"
:url="site.policy.url"
:lastdataupdate="site.policy.lastdataupdate"
:country="site.policy.country"
>
</PagePolicy>
</q-card-section>
</q-card>
</q-dialog>
<q-checkbox
v-model="signup.terms"
color="secondary"
@blur="v$.terms.$touch"
:error="v$.terms.$error"
:error-message="`${tools.errorMsg('terms', v$.terms)}`"
:label="$t('reg.terms')"
>
</q-checkbox>
<q-checkbox
v-if="showadultcheck"
v-model="iamadult"
color="secondary"
:label="$t('reg.onlyadult')"
>
</q-checkbox>
<div v-if="showadultcheck">
<br />
</div>
<!--
Già registrato?
<q-btn
class="q-ma-sm"
text-color="black"
color="white"
icon="fas fa-home"
label="Accedi"
to="/"
size="sm"
></q-btn>
<br /><br /><br />
-->
</q-carousel-slide>
</q-carousel>
</div>
<div class="row justify-center">
<q-btn-toggle
v-if="!signup.terms"
glossy
v-model="slide"
:options="[
{ label: 1, value: '1' },
{ label: 2, value: '2' },
{ label: 3, value: '3' },
{ label: 4, value: '4' },
]"
/>
</div>
</div>
</div>
<div v-else-if="isalreadyReg && !collettivo">
<q-banner
class="bg-negative text-white text-h5"
transition-show="jump-down"
>
Utente già registrato con l'username {{ signup.username }}
</q-banner>
</div>
</div>
</div>
</template>
<script lang="ts" src="./CSignUp.ts">
</script>
<style lang="scss" scoped>
@import './CSignUp.scss';
</style>

View File

@@ -1 +0,0 @@
export {default as CSignUp} from './CSignUp.vue'

View File

@@ -1,719 +1,24 @@
// ========================================
// 🎨 MODERN SIGNUP COMPONENT STYLES
// ========================================
// Variables
$primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
$success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
$error-gradient: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%);
$card-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
$card-shadow-hover: 0 15px 50px rgba(0, 0, 0, 0.15);
$border-radius: 20px;
$transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
// ========================================
// CONTAINER & LAYOUT
// ========================================
.signup-container {
min-height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 20px 15px 100px 15px; // Extra padding bottom for fixed buttons
position: relative;
@media (max-width: 768px) {
padding: 0px 10px 110px 10px;
}
}
.signup-content {
max-width: 500px;
width: 100%;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 0px;
}
// ========================================
// HEADER SECTION
// ========================================
.header-section {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
margin-bottom: 10px;
/* spazio tra logo e titolo */
animation: fadeInDown 0.6s ease-out;
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}
.title-section {
flex: 1;
margin-top: 5px;
.signup-title {
font-size: 2rem;
font-weight: 700;
background: $primary-gradient;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin: 0 0 5px 0;
@media (max-width: 768px) {
font-size: 1.5rem;
}
}
.signup-subtitle {
color: #64748b;
font-size: 13px;
margin: 0;
font-weight: 400;
@media (max-width: 768px) {
font-size: 0.75rem;
}
}
}
// ========================================
// SUCCESS & ERROR CARDS
// ========================================
.already-logged,
.error-card {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
}
.success-card,
.error-card {
background: white;
border-radius: $border-radius;
padding: 48px 32px;
box-shadow: $card-shadow;
text-align: center;
animation: scaleIn 0.5s ease-out;
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@media (max-width: 768px) {
padding: 32px 24px;
}
}
.success-message {
font-size: 24px;
font-weight: 600;
color: #1e293b;
margin-bottom: 32px;
@media (max-width: 768px) {
font-size: 20px;
margin-bottom: 24px;
}
}
.error-message {
font-size: 20px;
color: #1e293b;
margin-top: 16px;
@media (max-width: 768px) {
font-size: 18px;
}
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
.action-btn {
min-width: 160px;
height: 48px;
font-weight: 600;
text-transform: none;
letter-spacing: 0.3px;
@media (max-width: 768px) {
min-width: 140px;
height: 44px;
}
}
}
// ========================================
// FORM CONTAINER
// ========================================
.form-container {
background: white;
border-radius: $border-radius;
padding: 36px 24px 24px 24px;
box-shadow: $card-shadow;
transition: $transition;
animation: fadeInUp 0.6s ease-out;
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 768px) {
padding: 24px 16px 16px 16px;
border-radius: 16px;
}
}
.registration-form {
position: relative;
}
// ========================================
// PROGRESS INDICATOR
// ========================================
.progress-indicator {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40px;
position: relative;
padding: 0 20px;
@media (max-width: 768px) {
margin-bottom: 32px;
padding: 0 10px;
}
}
.progress-line {
position: absolute;
top: 20px;
left: 50px;
right: 50px;
height: 4px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 0;
border-radius: 2px;
@media (max-width: 768px) {
left: 35px;
right: 35px;
top: 18px;
}
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 1;
position: relative;
.step-circle {
width: 42px;
height: 42px;
border-radius: 50%;
background: #e2e8f0;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
color: #94a3b8;
transition: $transition;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
@media (max-width: 768px) {
width: 36px;
height: 36px;
font-size: 14px;
}
}
.step-label {
font-size: 13px;
font-weight: 600;
color: #94a3b8;
transition: $transition;
text-align: center;
@media (max-width: 768px) {
font-size: 11px;
}
}
&.active {
.step-circle {
background: $primary-gradient;
color: white;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.step-label {
color: #667eea;
font-weight: 700;
}
}
&.completed {
.step-circle {
background: $success-gradient;
color: white;
}
.step-label {
color: #11998e;
}
}
}
// ========================================
// CAROUSEL & SLIDES
// ========================================
.modern-carousel {
margin-bottom: 0;
min-height: 400px;
@media (max-width: 768px) {
min-height: 200px;
}
:deep(.q-carousel__slide) {
padding: 0;
}
}
.carousel-slide {
display: flex;
align-items: flex-start;
justify-content: center;
padding: 0 !important;
}
.slide-content {
width: 100%;
max-width: 100%;
animation: slideIn 0.4s ease-out;
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
}
.slide-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 12px;
margin-left: 6px;
margin-bottom: 32px;
@media (max-width: 768px) {
margin-bottom: 5px;
}
.q-icon {
margin-bottom: 16px;
opacity: 0;
animation: iconPop 0.6s ease-out 0.2s forwards;
@keyframes iconPop {
from {
opacity: 0;
transform: scale(0) rotate(-180deg);
}
to {
opacity: 1;
transform: scale(1) rotate(0deg);
}
}
@media (max-width: 768px) {
font-size: px !important;
margin-bottom: 0px;
}
}
.slide-title {
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin: 0;
@media (max-width: 768px) {
font-size: 22px;
}
}
.slide-description {
font-size: 15px;
color: #64748b;
margin: 0;
font-weight: 400;
@media (max-width: 768px) {
font-size: 14px;
}
}
}
// ========================================
// FORM FIELDS
// ========================================
.form-fields {
display: flex;
flex-direction: column;
gap: 20px;
@media (max-width: 768px) {
gap: 15px;
}
}
.name-fields {
display: flex;
flex-direction: column;
gap: 20px;
@media (max-width: 768px) {
gap: 0px;
}
}
.modern-input {
:deep(.q-field__control) {
height: 56px;
border-radius: 14px;
background: #f8fafc;
transition: $transition;
&:hover {
background: #f1f5f9;
}
@media (max-width: 768px) {
height: 52px;
border-radius: 12px;
}
}
:deep(.q-field__label) {
font-weight: 500;
font-size: 14px;
}
:deep(.q-field__control)::before {
border-color: #e2e8f0;
}
:deep(.q-field__control):hover::before {
border-color: #cbd5e1;
}
:deep(.q-field--focused .q-field__control)::before {
border-color: #667eea;
border-width: 2px;
}
:deep(.q-field--filled .q-field__control) {
background: #f8fafc;
}
:deep(.q-field__prepend) {
.q-icon {
font-size: 22px;
margin-right: 4px;
}
}
:deep(.q-field__append) {
.q-btn {
opacity: 0.6;
transition: $transition;
&:hover {
opacity: 1;
}
}
}
}
// ========================================
// PRIVACY SECTION
// ========================================
.privacy-section {
margin-top: 24px;
@media (max-width: 768px) {
margin-top: 20px;
}
.q-separator {
background: #e2e8f0;
margin: 24px 0;
@media (max-width: 768px) {
margin: 20px 0;
}
}
}
.privacy-link {
text-align: center;
margin-bottom: 16px;
.q-btn {
font-weight: 500;
text-transform: none;
font-size: 14px;
&:hover {
background: rgba(102, 126, 234, 0.08);
}
}
}
.privacy-checkbox {
:deep(.q-checkbox__label) {
font-size: 14px;
line-height: 1.3;
color: #475569;
font-weight: 500;
}
:deep(.q-checkbox__inner) {
width: 22px;
height: 22px;
border-radius: 6px;
}
@media (max-width: 768px) {
:deep(.q-checkbox__label) {
font-size: 13px;
}
}
}
// ========================================
// FIXED BOTTOM ACTIONS
// ========================================
.fixed-bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
padding: 16px 20px;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.08);
z-index: 1000;
backdrop-filter: blur(10px);
border-top: 1px solid #e2e8f0;
@media (max-width: 768px) {
padding: 14px 16px;
}
}
.action-row {
max-width: 500px;
margin: 0 auto;
display: flex;
gap: 12px;
justify-content: space-between;
align-items: center;
@media (max-width: 768px) {
gap: 10px;
}
}
.nav-btn {
font-weight: 600;
text-transform: none;
letter-spacing: 0.3px;
height: 50px;
border-radius: 14px;
transition: $transition;
font-size: 15px;
@media (max-width: 768px) {
height: 46px;
border-radius: 12px;
font-size: 14px;
}
&:hover:not([disabled]) {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
&:active:not([disabled]) {
transform: translateY(0);
}
&[disabled] {
opacity: 0.5;
}
}
.prev-btn {
flex: 0 0 auto;
min-width: 130px;
@media (max-width: 768px) {
min-width: 110px;
}
}
.next-btn {
flex: 1;
background: $primary-gradient;
&:hover:not([disabled]) {
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
}
.submit-btn {
flex: 1;
background: $success-gradient;
font-size: 16px;
font-weight: 700;
&:hover:not([disabled]) {
box-shadow: 0 6px 20px rgba(17, 153, 142, 0.4);
}
@media (max-width: 768px) {
font-size: 15px;
}
}
// ========================================
// TELEGRAM SETUP
// ========================================
.telegram-setup,
.telegram-registration {
text-align: center;
padding: 48px 24px;
background: white;
border-radius: $border-radius;
box-shadow: $card-shadow;
@media (max-width: 768px) {
padding: 32px 20px;
}
.telegram-message {
font-size: 18px;
color: #475569;
margin: 24px 0;
line-height: 1.3;
@media (max-width: 768px) {
font-size: 16px;
margin: 20px 0;
}
}
.telegram-btn {
min-width: 200px;
height: 54px;
font-size: 16px;
font-weight: 600;
text-transform: none;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
}
@media (max-width: 768px) {
min-width: 180px;
height: 50px;
font-size: 15px;
}
}
}
// ========================================
// UTILITIES
// ========================================
.signup {
width: 100%;
margin: 0 auto;
max-width: 500px;
max-width: 450px;
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
}
// Legacy classes (kept for compatibility)
.clCellCode,
.clCellCode {
border-radius: 32px;
border-right: #2d2260;
height: 50px;
font-size: 1rem;
padding: 8px;
}
.clCell {
border-radius: 32px;
border-right: #2d2260;
@@ -722,43 +27,20 @@ $transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
padding: 8px;
}
.vue-country-select {
.vue-country-select{
border-radius: 32px;
}
.myuserinvitante {
.myuserinvitante{
font-weight: bold;
color: #667eea;
color: red;
font-size: 1.5rem;
}
.cosa_chiedere {
font-weight: 600;
color: #667eea;
.cosa_chiedere{
font-weight: bold;
color: blue;
font-size: 1rem;
padding: 10px;
text-align: center;
}
// ========================================
// ANIMATIONS & TRANSITIONS
// ========================================
.q-carousel__slide {
animation: fadeSlide 0.3s ease-out;
@keyframes fadeSlide {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
}
// Smooth scroll behavior
* {
scroll-behavior: smooth;
}

View File

@@ -1,30 +1,42 @@
import { tools } from '@tools'
import { tools } from '@tools';
import type { ISignupOptions } from 'model'
import type { ISignupOptions } from 'model';
import { Logo } from '@src/components/logo'
import { Logo } from '@src/components/logo';
import { CTitleBanner } from '../CTitleBanner'
import { CCopyBtn } from '../CCopyBtn'
import { CRegistration } from '../CRegistration'
import { PagePolicy } from '../PagePolicy'
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue'
import { CSignIn } from '@src/components/CSignIn'
import { useQuasar } from 'quasar'
import { useI18n } from 'vue-i18n'
import { DefaultProfile, useUserStore } from '@store/UserStore'
import useValidate from '@vuelidate/core'
import useVuelidate from '@vuelidate/core'
// import 'vue-country-code/dist/vue-country-code.css'
import { shared_consts } from '@src/common/shared_vuejs'
import { CTitleBanner } from '../CTitleBanner';
import { CCopyBtn } from '../CCopyBtn';
import { CRegistration } from '../CRegistration';
import { PagePolicy } from '../PagePolicy';
import { computed, defineComponent, onMounted, reactive, ref, watch } from 'vue';
import { CSignIn } from '@src/components/CSignIn';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { DefaultProfile, useUserStore } from '@store/UserStore';
import useValidate from '@vuelidate/core';
import useVuelidate from '@vuelidate/core';
import { minLength, required, sameAs } from '@vuelidate/validators'
import { shared_consts } from '@src/common/shared_vuejs';
import { complexity, complexityUser, registereduser, aportadorexist } from '../../validation'
import { minLength, required, sameAs } from '@vuelidate/validators';
import { useRoute, useRouter } from 'vue-router'
import { static_data } from '@src/db/static_data'
import { useGlobalStore } from '@store/globalStore'
// import { ValidationRuleset } from 'vuelidate'
import {
complexity,
complexityUser,
registereduser,
aportadorexist,
} from '../../validation';
// import 'vue3-tel-input/dist/vue3-tel-input.css'
import { useRoute, useRouter } from 'vue-router';
import { static_data } from '@src/db/static_data';
import { useGlobalStore } from '@store/globalStore';
import { useInvitaAmicoStore } from 'app/src/stores/useInvitaAmicoStore';
// import {Loading, QSpinnerFacebook, QSpinnerGears} from 'quasar'
export default defineComponent({
name: 'CSignUp',
@@ -80,59 +92,78 @@ export default defineComponent({
required: false,
default: false,
},
token: {
type: String,
required: false,
default: '',
},
},
setup(props, { emit }) {
const $q = useQuasar()
const { t } = useI18n()
const userStore = useUserStore()
const $route = useRoute()
const $router = useRouter()
const $q = useQuasar();
const { t } = useI18n();
const userStore = useUserStore();
const $route = useRoute();
const $router = useRouter();
const countryname = ref('')
const iamadult = ref(false)
const duplicate_email = ref(false)
const duplicate_username = ref(false)
const visureg = ref(false)
const showpolicy = ref(false)
const visubuttBOT = ref(false)
const isalreadyReg = ref(false)
const needTelegram = ref(false)
const slide = ref('1')
const inputAportador = ref(<any>null)
const inputEmail = ref(<any>null)
const inputUsername = ref(<any>null)
const inputName = ref(<any>null)
const inputSurname = ref(<any>null)
const inputPassword = ref(<any>null)
const inputPassword2 = ref(<any>null)
const countryname = ref('');
const iamadult = ref(false);
const duplicate_email = ref(false);
const duplicate_username = ref(false);
const visureg = ref(false);
const showpolicy = ref(false);
const visubuttBOT = ref(false);
const isalreadyReg = ref(false);
const needTelegram = ref(false);
const slide = ref('1');
const inputAportador = ref(<any>null);
const inputEmail = ref(<any>null);
const inputUsername = ref(<any>null);
const inputName = ref(<any>null);
const inputSurname = ref(<any>null);
const inputPassword = ref(<any>null);
const inputPassword2 = ref(<any>null);
const invitaStore = useInvitaAmicoStore();
const checkifDisabled = computed(() => {
let ret = true
let ret = true;
if (slide.value === '1') {
// Email + Aportador
ret = !signup.email || (tools.getAskToVerifyReg() && (!signup.aportador_solidario || inputAportador.value?.hasError)) || (inputEmail.value && inputEmail.value.hasError)
// Invitante + Email
ret =
!signup.email ||
(tools.getAskToVerifyReg() &&
(!signup.aportador_solidario || inputAportador.value.hasError)) ||
(inputEmail.value && inputEmail.value.hasError);
} else if (slide.value === '2') {
// Username + Nome/Cognome
ret = !signup.username || (inputUsername.value && inputUsername.value.hasError)
// Username
ret = !signup.username || (inputUsername.value && inputUsername.value.hasError);
if (tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) {
ret = ret || (!signup.name || (inputName.value && inputName.value.hasError))
ret = ret || (!signup.surname || (inputSurname.value && inputSurname.value.hasError))
if (
tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)
) {
ret = ret || !signup.name || (inputName.value && inputName.value.hasError);
ret =
ret || !signup.surname || (inputSurname.value && inputSurname.value.hasError);
}
} else if (slide.value === '3') {
// Password + Ripeti Password
ret = !signup.password || (!inputPassword.value || (inputPassword.value && inputPassword.value.hasError)) || (!inputPassword2.value || (inputPassword2.value && inputPassword2.value.hasError))
// Password
ret =
!signup.password ||
!inputPassword.value ||
(inputPassword.value && inputPassword.value.hasError) ||
!inputPassword2.value ||
(inputPassword2.value && inputPassword2.value.hasError);
}
return ret
})
return ret;
});
const typePassword = ref('password')
const typePassword = ref('password');
const ap_iniziale = ref('')
const ap_iniziale = ref('');
const globalStore = useGlobalStore()
const site = computed(() => globalStore.site)
const globalStore = useGlobalStore();
const site = computed(() => globalStore.site);
const signup = reactive(<ISignupOptions>{
email: '',
@@ -141,10 +172,10 @@ export default defineComponent({
surname: '',
password: '',
repeatPassword: '',
terms: true, // ✅ GIÀ SPUNTATA DI DEFAULT
terms: true,
profile: DefaultProfile,
aportador_solidario: '',
})
});
const validations: any = computed(() => {
const valid: any = {
@@ -164,198 +195,278 @@ export default defineComponent({
registereduser,
},
name: {
required: (props.collettivo || tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) ? true : false,
required:
props.collettivo ||
tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)
? true
: false,
},
surname: {
required: (tools.getConfSiteOptionEnabled(shared_consts.ConfSite.regNameSurnameMandatory)) ? true : false,
required: tools.getConfSiteOptionEnabled(
shared_consts.ConfSite.regNameSurnameMandatory
)
? true
: false,
},
terms: {
required,
},
aportador_solidario: {
aportadorexist,
required
}
}
required,
},
};
if (props.show_namesurname) {
valid.name = {}
valid.surname = {}
valid.name = {};
valid.surname = {};
}
return valid
})
return valid;
});
// @ts-ignore
const v$ = useVuelidate(validations, signup)
const v$ = useVuelidate(validations, signup);
const invited = ref($route.params.invited)
const usernameteleg = ref($route.params.usernameteleg)
const idteleg = ref($route.params.idteleg)
const invited = ref($route.params.invited);
const usernameteleg = ref($route.params.usernameteleg);
const idteleg = ref($route.params.idteleg);
watch(() => slide.value, (to: any, from: any) => {
if (slide.value === '3') {
v$.value.$touch()
watch(
() => slide.value,
(to: any, from: any) => {
if (slide.value === '3') {
v$.value.$touch();
}
}
})
);
watch(() => invited, (to: any, from: any) => {
if (props.showaportador) {
console.log('changeaportador', $route.params.invited)
if (!signup.aportador_solidario) {
if ($route.params.invited) {
// @ts-ignore
signup.aportador_solidario = $route.params.invited
watch(
() => invited,
(to: any, from: any) => {
if (props.showaportador) {
console.log('changeaportador', $route.params.invited);
if (!signup.aportador_solidario) {
if ($route.params.invited) {
// @ts-ignore
signup.aportador_solidario = $route.params.invited;
}
}
}
}
})
);
function allowSubmit() {
let error = v$.value.$error || v$.value.$invalid || globalStore.serverError
let error = v$.value.$error || v$.value.$invalid || globalStore.serverError;
if (props.showadultcheck)
error = error || !iamadult.value
if (props.showadultcheck) error = error || !iamadult.value;
if (props.showcell) {
if (signup.profile)
error = error || signup.profile.cell!.length <= 6
else
error = true
if (signup.profile) error = error || signup.profile.cell!.length <= 6;
else error = true;
}
if (tools.getAskToVerifyReg()) {
error = error || !signup.aportador_solidario
error = error || !signup.aportador_solidario;
}
return !error
return !error;
}
function env() {
return process.env
return process.env;
}
function changeemail() {
signup.email = tools.removespaces(signup.email!)
signup.email = signup.email.toLowerCase()
emit('update:value', signup.email)
signup.email = tools.removespaces(signup.email!);
signup.email = signup.email.toLowerCase();
emit('update:value', signup.email);
}
function changeusername(value: string) {
signup.username = tools.removespaces(signup.username)
emit('update:value', signup.username)
signup.username = tools.removespaces(signup.username);
emit('update:value', signup.username);
}
function submitOk() {
v$.value.$touch()
v$.value.$touch();
signup.email = tools.removespaces(signup.email!)
signup.email = signup.email.toLowerCase()
signup.username = tools.removespaces(signup.username)
signup.email = tools.removespaces(signup.email!);
signup.email = signup.email.toLowerCase();
signup.username = tools.removespaces(signup.username);
// remove @
signup.username = tools.removeAt(signup.username)
signup.username = tools.removeAt(signup.username);
duplicate_email.value = false
duplicate_username.value = false
duplicate_email.value = false;
duplicate_username.value = false;
if (!signup.terms) {
tools.showNotif($q, t('reg.err.terms'))
return
tools.showNotif($q, t('reg.err.terms'));
return;
}
/*if (v$.signup.$error) {
tools.showNotif($q, t('reg.err.errore_generico'))
return
} */
if (signup.name) {
signup.name = tools.CapitalizeAllWords(signup.name)
signup.surname = tools.CapitalizeAllWords(signup.surname)
signup.name = tools.CapitalizeAllWords(signup.name);
signup.surname = tools.CapitalizeAllWords(signup.surname);
}
$q.loading.show({ message: t('reg.incorso') })
$q.loading.show({ message: t('reg.incorso') });
console.log(signup)
return userStore.signup(tools.clone(signup))
console.log(signup);
return userStore
.signup(tools.clone(signup))
.then((ris: any) => {
if (tools.SignUpcheckErrors($q, $router, ris.code, ris.msg))
$q.loading.hide()
}).catch((error: string) => {
console.log('ERROR = ' + error)
$q.loading.hide()
if (tools.SignUpcheckErrors($q, $router, ris.code, ris.msg)) $q.loading.hide();
})
.catch((error: string) => {
console.log('ERROR = ' + error);
$q.loading.hide();
});
}
function intcode_change(coderec: any) {
// console.log('intcode', coderec)
if (signup.profile) {
signup.profile.intcode_cell = '+' + coderec.dialCode
signup.profile.iso2_cell = coderec.iso2
signup.profile.intcode_cell = '+' + coderec.dialCode;
signup.profile.iso2_cell = coderec.iso2;
}
}
function selectcountry({ name, iso2, dialCode }: { name: string, iso2: string, dialCode: string }) {
signup.profile.nationality = iso2
countryname.value = name
function selectcountry({
name,
iso2,
dialCode,
}: {
name: string;
iso2: string;
dialCode: string;
}) {
// console.log(name, iso2, dialCode)
signup.profile.nationality = iso2;
countryname.value = name;
}
async function created() {
needTelegram.value = props.need_Telegram
needTelegram.value = props.need_Telegram;
console.log('$route.params', $route.params)
console.log('$route.params', $route.params);
ap_iniziale.value = $route.params.invited ? $route.params.invited.toString() : ''
ap_iniziale.value = $route.params.invited ? $route.params.invited.toString() : '';
signup.aportador_solidario = $route.params.invited ? $route.params.invited.toString() : ''
signup.username = $route.params.usernameteleg ? $route.params.usernameteleg.toString() : ''
signup.regexpire = $route.params.regexpire ? $route.params.regexpire.toString() : props.regexpire
signup.aportador_solidario = $route.params.invited
? $route.params.invited.toString()
: '';
signup.username = $route.params.usernameteleg
? $route.params.usernameteleg.toString()
: '';
signup.regexpire = $route.params.regexpire
? $route.params.regexpire.toString()
: props.regexpire;
if (signup.username)
isalreadyReg.value = await tools.registeredusername(signup.username)
signup.profile.username_telegram = signup.username
isalreadyReg.value = await tools.registeredusername(signup.username);
signup.profile.username_telegram = signup.username;
if ($route.params.idteleg) {
signup.profile.teleg_id = $route.params.idteleg ? parseInt($route.params.idteleg.toString(), 10) : 0
signup.profile.teleg_id = $route.params.idteleg
? parseInt($route.params.idteleg.toString(), 10)
: 0;
}
if (props.collettivo) {
signup.username = props.username_default!
signup.name = props.name_default!
signup.username = props.username_default!;
signup.name = props.name_default!;
}
// console.log('1) aportador_solidario', signup.aportador_solidario)
if (!signup.aportador_solidario)
signup.aportador_solidario = tools.getCookie(tools.APORTADOR_SOLIDARIO, signup.aportador_solidario)
signup.aportador_solidario = tools.getCookie(
tools.APORTADOR_SOLIDARIO,
signup.aportador_solidario
);
if (!signup.aportador_solidario || signup.aportador_solidario === 'undefined') {
if (!tools.getAskToVerifyReg()) {
signup.aportador_solidario = tools.APORTADOR_NONE
signup.aportador_solidario = tools.APORTADOR_NONE;
}
}
// console.log('signup.aportador_solidario', signup.aportador_solidario)
// console.log('getasktoverify', tools.getAskToVerifyReg())
if (tools.getAskToVerifyReg()) {
if (!signup.username || !signup.profile.teleg_id) {
visubuttBOT.value = true
// tools.copyStringToClipboard($q, signup.aportador_solidario, true)
visubuttBOT.value = true;
// window.location.href = tools.getLinkBotTelegram()
}
}
}
function myRuleEmail(val: string) {
return new Promise((resolve, reject) => {
// call
// resolve(true)
// --> content is valid
// resolve(false)
// --> content is NOT valid, no error message
// resolve(error_message)
// --> content is NOT valid, we have error message
tools.registeredemail(val).then((emailOk) => {
let risp = !!emailOk || t('reg.err.duplicate_email')
let risp = !!emailOk || t('reg.err.duplicate_email');
if (emailOk) {
risp = tools.isEmail(val) || t('reg.err.invalid_email')
emailOk = emailOk && tools.isEmail(val)
risp = tools.isEmail(val) || t('reg.err.invalid_email');
emailOk = emailOk && tools.isEmail(val);
}
resolve(risp)
})
})
if (emailOk) {
// risp = !tools.isEmailNoMicroZozz(val) || t('reg.err.invalid_email_micro')
}
resolve(risp);
});
// calling reject(...) will also mark the input
// as having an error, but there will not be any
// error message displayed below the input
// (only in browser console)
});
}
function showPassword() {
typePassword.value = typePassword.value === 'password' ? 'text' : 'password'
//
typePassword.value = typePassword.value === 'password' ? 'text' : 'password';
}
function regEventEmail(invited: boolean) {
console.log('EVENT RECEIVED: regEventEmail', invited)
visubuttBOT.value = false
needTelegram.value = false
console.log('EVENT RECEIVED: regEventEmail', invited);
// reg
visubuttBOT.value = false;
needTelegram.value = false;
}
created()
onMounted(async () => {
const token = props.token;
if (token) {
// carica le info della registrazione
const risinvite = await invitaStore.ottieniInvitoByToken(token);
if (risinvite && risinvite.email) {
signup.email = risinvite.email;
if (risinvite.usernameInvitante)
signup.aportador_solidario = risinvite.usernameInvitante
slide.value = '2'
}
}
});
created();
return {
changeemail,
@@ -391,6 +502,6 @@ export default defineComponent({
inputPassword,
inputPassword2,
shared_consts,
}
};
},
})
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,311 +0,0 @@
# 🎨 Componente CSignUp - Versione Migliorata
## ✨ Modifiche Principali
### 📊 Struttura Carousel (Da 4 a 3 Pagine)
#### **Pagina 1 - Dati Iniziali**
- ✉️ Email (campo principale)
- 👤 Aportador/Invitante (se necessario)
- 🎯 Focus: Raccolta dati di contatto
#### **Pagina 2 - Account**
- 👤 Username
- 📛 Nome e Cognome (se richiesto)
- 🎯 Focus: Identità utente
#### **Pagina 3 - Sicurezza e Privacy**
- 🔒 Password
- 🔒 Conferma Password
- ✅ Privacy Policy (GIÀ SPUNTATA)
- 🔞 Verifica Maggiore Età (se richiesto)
- 🎯 Focus: Sicurezza e consensi
---
## 🎨 Miglioramenti Estetici
### Design Moderno e Innovativo
- **Gradients Accattivanti**: Utilizzo di gradienti moderni per un look premium
- **Card con Shadow**: Effetti ombra eleganti per dare profondità
- **Animazioni Fluide**: Transizioni smooth tra le pagine
- **Icone Prominenti**: Icone grandi e colorate per guidare l'utente
- **Progress Indicator**: Barra di progresso visiva con step animati
### Color Scheme
```scss
Primary Gradient: #667eea #764ba2 (Viola/Blu)
Success Gradient: #11998e #38ef7d (Verde)
Error Gradient: #ee0979 #ff6a00 (Rosso/Arancio)
Background: #f5f7fa #c3cfe2 (Grigio chiaro)
```
---
## 📱 Responsive Design
### Breakpoints Ottimizzati
- **Desktop** (>768px): Layout completo con spaziature generose
- **Mobile** (<768px): Layout ottimizzato con dimensioni ridotte
### Caratteristiche Responsive
- Input con altezza adattiva (56px → 52px su mobile)
- Font size scalabili
- Padding e margini ottimizzati per ogni device
- Bottoni che si adattano alla larghezza disponibile
---
## 📍 Bottoni Fissi in Basso
### Implementazione
```scss
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
```
### Caratteristiche
- **Sempre Visibili**: I bottoni restano fissi durante lo scroll
- **Design Elegante**: Shadow e backdrop blur per un effetto premium
- **Azioni Contestuali**:
- Pagina 1-2: "Indietro" + "Continua"
- Pagina 3: "Indietro" + "Registrati" (verde, più grande)
### Animazioni Bottoni
- Hover: Movimento verso l'alto con shadow aumentata
- Click: Feedback visivo immediato
- Disabled: Opacità ridotta
---
## ☑️ Privacy Già Spuntata
### Implementazione nel TypeScript
```typescript
const signup = reactive(<ISignupOptions>{
// ... altri campi
terms: true, // ✅ GIÀ SPUNTATA DI DEFAULT
})
```
### User Experience
1. L'utente arriva alla pagina 3
2. La checkbox privacy è già selezionata
3. Link "Leggi la Privacy Policy" disponibile
4. L'utente può deselezionare se vuole (ma non può procedere)
---
## 👆 UX Ottimizzata
### Principi di Design Applicati
#### 1. **Minimo Effort**
- Campi ridotti al necessario
- Privacy pre-accettata
- 3 pagine invece di 4 (-25% di navigazione)
#### 2. **No Scroll Required**
- Altezza carousel ottimizzata (400px desktop, 360px mobile)
- Contenuto sempre visibile in una schermata
- Bottoni fissi eliminano necessità di scroll
#### 3. **Progressive Disclosure**
- Informazioni presentate gradualmente
- Step chiari e ben definiti
- Progress indicator sempre visibile
#### 4. **Error Prevention**
- Validazione real-time
- Bottoni disabilitati quando i dati non sono validi
- Messaggi di errore chiari e inline
#### 5. **Keyboard Navigation**
- Enter per passare al campo successivo
- Enter nell'ultimo campo per avanzare pagina
- Tab navigation ottimizzata
---
## 🎯 Caratteristiche Tecniche
### Progress Indicator Dinamico
```vue
<div class="progress-indicator">
<div v-for="step in 3" :key="step"
:class="{ active: parseInt(slide) >= step,
completed: parseInt(slide) > step }">
<!-- Step circle con animazioni -->
</div>
<div class="progress-line"
:style="{ width: `${(parseInt(slide) - 1) * 50}%` }">
</div>
</div>
```
### Validazione Intelligente
- Campo Email: Validazione asincrona per email duplicate
- Campo Username: Check real-time su disponibilità
- Password: Controllo complessità con feedback visivo
- Conferma Password: Match immediato
### Animazioni CSS
- **fadeInDown**: Header (0.6s)
- **fadeInUp**: Form container (0.6s)
- **scaleIn**: Success/Error cards (0.5s)
- **slideIn**: Contenuto slide (0.4s)
- **iconPop**: Icone header (0.6s con delay)
---
## 📦 File Modificati
### 1. CSignUp.vue
- ✅ Ridotto carousel a 3 slide
- ✅ Nuovo layout con progress indicator
- ✅ Bottoni fissi in basso
- ✅ Privacy section nell'ultima slide
- ✅ Header con icone e titoli descrittivi
- ✅ Animazioni fluide tra le slide
### 2. CSignUp.ts
-`terms: true` di default
- ✅ Logica di validazione per 3 slide
- ✅ Gestione keyboard navigation ottimizzata
### 3. CSignUp.scss
- ✅ Design system completo con variabili
- ✅ Gradients e colori moderni
- ✅ Animazioni e transizioni
- ✅ Responsive breakpoints
- ✅ Fixed bottom buttons styling
- ✅ Progress indicator styling
---
## 🚀 Come Usare
### Installazione
1. Sostituisci i file esistenti con le nuove versioni
2. Non sono richieste dipendenze aggiuntive
3. Il componente è retrocompatibile con le props esistenti
### Props Disponibili
Tutte le props originali sono mantenute:
- `showadultcheck`: Mostra checkbox maggiorenne
- `showcell`: Mostra campo telefono
- `showaportador`: Mostra campo invitante
- `shownationality`: Mostra campo nazionalità
- `show_namesurname`: Mostra campi nome/cognome
- `need_Telegram`: Richiede registrazione Telegram
- `collettivo`: Modalità collettivo
---
## 📊 Metriche di Miglioramento
| Metrica | Prima | Dopo | Miglioramento |
|---------|-------|------|---------------|
| Pagine Carousel | 4 | 3 | -25% |
| Click per Completamento | 6-8 | 4-5 | ~40% |
| Tempo Medio Registrazione | ~120s | ~80s | ~33% |
| Privacy Pre-accettata | ❌ | ✅ | +100% |
| Mobile Friendly | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +66% |
---
## 🎨 Design Tokens
### Spacing
```scss
Small: 8px, 12px
Medium: 16px, 20px, 24px
Large: 32px, 40px, 48px
```
### Typography
```scss
Title: 32px / 28px (mobile)
Subtitle: 16px / 14px (mobile)
Slide Title: 26px / 22px (mobile)
Body: 15px / 14px (mobile)
```
### Border Radius
```scss
Small: 12px
Medium: 14px
Large: 20px
Full: 50% (circles)
```
---
## 🔧 Personalizzazione
### Cambiare i Colori
Modifica le variabili in `CSignUp.scss`:
```scss
$primary-gradient: linear-gradient(135deg, #TUO_COLORE_1 0%, #TUO_COLORE_2 100%);
$success-gradient: linear-gradient(135deg, #TUO_COLORE_1 0%, #TUO_COLORE_2 100%);
```
### Cambiare le Animazioni
Modifica i timing delle animazioni:
```scss
$transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
```
### Cambiare l'Altezza del Carousel
```scss
.modern-carousel {
min-height: 400px; // Modifica qui
}
```
---
## 🐛 Note e Compatibilità
### Compatibilità
- ✅ Vue 3
- ✅ Quasar Framework
- ✅ TypeScript
- ✅ Vuelidate
- ✅ Mobile & Desktop
### Browser Support
- ✅ Chrome/Edge (ultime 2 versioni)
- ✅ Firefox (ultime 2 versioni)
- ✅ Safari (ultime 2 versioni)
- ✅ Mobile browsers
---
## 📝 Changelog
### Versione 2.0 (Attuale)
- ✨ Ridotto carousel da 4 a 3 pagine
- ✨ Design completamente rinnovato
- ✨ Progress indicator con animazioni
- ✨ Bottoni fissi in basso
- ✨ Privacy pre-accettata
- ✨ UX ottimizzata senza scroll
- ✨ Completamente responsive
- ✨ Nuove animazioni e transizioni
### Versione 1.0 (Precedente)
- Form di registrazione base
- 4 pagine carousel
- Design classico
---
## 📧 Supporto
Per domande o problemi, contatta il team di sviluppo.
**Made with ❤️ for better UX**

View File

@@ -37,6 +37,7 @@ $text-light: #666; // Grigio medio
align-items: center;
justify-content: center;
gap: 12px;
line-height: 1.4;
&.light {
color: white !important;
@@ -49,6 +50,14 @@ $text-light: #666; // Grigio medio
.title-icon {
color: $primary-color;
}
@media (max-width: 768px) {
font-size: 1.5rem;
margin-bottom: 1rem;
margin-top: 1rem !important;
line-height: 1.6rem;
}
}
// HomeRiso.scss - Aggiorna la sezione HERO
@@ -493,6 +502,9 @@ $text-light: #666; // Grigio medio
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2.5rem;
@media (max-width: 768px) {
gap: 1rem;
}
}
.value-item {
@@ -506,6 +518,10 @@ $text-light: #666; // Grigio medio
transform: translateY(-8px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
padding: 0.5rem;
}
}
.value-icon {
@@ -552,6 +568,11 @@ $text-light: #666; // Grigio medio
&:last-child {
margin-bottom: 0;
}
@media (max-width: 768px) {
margin-bottom: 1rem;
gap: 0rem;
}
}
.step-number {
@@ -607,12 +628,18 @@ $text-light: #666; // Grigio medio
text-align: center;
max-width: 800px;
margin: 0 auto 3rem;
@media (max-width: 768px) {
margin: 0 auto 1rem;
}
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 2rem;
@media (max-width: 768px) {
gap: 0.5rem;
}
}
.feature-card {
@@ -712,6 +739,10 @@ $text-light: #666; // Grigio medio
color: $primary-dark;
text-align: center;
margin-bottom: 2rem;
@media (max-width: 768px) {
font-size: 1.6rem;
margin-bottom: 1rem;
}
}
.methods-grid {
@@ -1110,9 +1141,6 @@ $text-light: #666; // Grigio medio
font-size: 3.5rem;
}
.section-title {
font-size: 2rem;
}
}
@media (max-width: 768px) {
@@ -1130,10 +1158,6 @@ $text-light: #666; // Grigio medio
font-size: 1rem;
}
.section-title {
font-size: 1.8rem;
}
.cta-title {
font-size: 2rem;
}

View File

@@ -194,7 +194,7 @@
size="md"
class="title-icon"
/>
Guarda il Video di Presentazione
Video di Presentazione
</h2>
<div class="video-container">
@@ -793,7 +793,7 @@
<q-separator class="footer-separator" />
<div class="footer-bottom">
<p>{{ currentYear }} RISO - Rete Italiana di Scambio Orizzontale</p>
<p>(dal 2021) RISO - Rete Italiana di Scambio Orizzontale</p>
<p class="footer-values">
Comunità · Fiducia · Scambi Solidali · Sostenibilità
</p>

View File

@@ -2,17 +2,21 @@
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
min-height: 120vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
@media (max-width: $breakpoint-sm-max) {
min-height: 100vh;
}
}
.invita-amico-card {
max-width: 600px;
max-width: 800px;
width: 100%;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
@media (max-width: $breakpoint-sm-max) {
max-width: 600px;
margin: 0;
border-radius: 0;
}
@@ -47,3 +51,50 @@
border-radius: 8px;
overflow: hidden;
}
// Bottoni selezione metodo
.selection-buttons {
display: flex;
gap: 16px;
flex-wrap: wrap;
@media (max-width: $breakpoint-xs-max) {
flex-direction: column;
}
}
.selection-btn {
flex: 1;
min-height: 180px;
border-radius: 12px;
border: 2px solid #e0e0e0;
background: white;
transition: all 0.3s ease;
&:hover {
border-color: var(--q-primary);
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.2);
transform: translateY(-4px);
}
&:active {
transform: translateY(-2px);
}
@media (max-width: $breakpoint-xs-max) {
min-height: 120px;
}
}
.selection-btn-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
width: 100%;
text-align: center;
@media (max-width: 768px) {
padding: 8px 24px;
}
}

View File

@@ -3,6 +3,7 @@ import { useQuasar } from 'quasar';
import { useInvitaAmicoStore } from '../../stores/useInvitaAmicoStore';
import type { InvitoAmicoForm } from '../../types/invita-amico.types.ts';
import { tools } from 'app/src/store/Modules/tools';
import { useI18n } from 'vue-i18n';
// Chiave localStorage
const MESSAGGIO_STORAGE_KEY = 'invita-amico-messaggio-personalizzato';
@@ -15,10 +16,12 @@ export default defineComponent({
setup(props, { emit }) {
// Composables
const $q = useQuasar();
const { t } = useI18n();
const invitaStore = useInvitaAmicoStore();
// State
const mostraCronologia = ref(false);
const metodoSelezionato = ref<'email' | 'telegram' | null>(null);
const form = reactive<InvitoAmicoForm & { usernameInvitante?: string }>({
email: '',
messaggio: '',
@@ -29,6 +32,20 @@ export default defineComponent({
// METHODS
// ==========================================
/**
* Seleziona il metodo di invio (email o telegram)
*/
const selezionaMetodo = (metodo: 'email' | 'telegram') => {
metodoSelezionato.value = metodo;
};
/**
* Torna alla schermata di scelta iniziale
*/
const tornaAllaScelta = () => {
metodoSelezionato.value = null;
};
/**
* Invia invito via email usando lo store Pinia
*/
@@ -51,7 +68,7 @@ export default defineComponent({
message: 'Invito inviato con successo! 🎉',
caption: `L'email è stata inviata a ${form.email}`,
icon: 'check_circle',
timeout: 3000,
timeout: 7000,
actions: [
{
label: 'Vedi cronologia',
@@ -86,14 +103,14 @@ export default defineComponent({
const onInviaTelegram = async () => {
emit('telegram-click');
const success = await invitaStore.inviaInvitoTelegram(form.messaggio);
const success = await invitaStore.inviaInvitoTelegram($q, t);
if (success) {
$q.notify({
type: 'positive',
message: 'Messaggio inviato via Telegram! ✈️',
icon: 'telegram',
timeout: 2000,
timeout: 4000,
});
} else {
$q.notify({
@@ -122,7 +139,7 @@ export default defineComponent({
type: 'info',
message: 'Cronologia cancellata',
icon: 'delete',
timeout: 2000,
timeout: 3000,
});
});
};
@@ -168,6 +185,9 @@ export default defineComponent({
// RETURN
return {
mostraCronologia,
metodoSelezionato,
selezionaMetodo,
tornaAllaScelta,
form,
onInviaEmail,
onInviaTelegram,

View File

@@ -12,47 +12,78 @@
/>
Invita un Amico
</div>
<div class="text-subtitle2">Condividi la nostra app con i tuoi amici!</div>
<!-- Stats Badge -->
<div
v-if="invitaStore.totaleInviti > 0"
class="stats-badge q-mt-md"
>
<q-chip
color="white"
text-color="primary"
icon="email"
class="q-mx-xs"
>
{{ invitaStore.contatoreInvitiRiusciti }} inviati
</q-chip>
<!--<q-chip
v-if="invitaStore.percentualeSuccesso > 0"
color="white"
text-color="primary"
icon="trending_up"
class="q-mx-xs"
>
{{ invitaStore.percentualeSuccesso }}% successo
</q-chip>-->
</div>
<div class="text-subtitle2">Condividi la app con i tuoi amici!</div>
</q-card-section>
<q-separator />
<!-- Form Section -->
<q-card-section>
<q-form
@submit="onInviaEmail"
class="q-gutter-md"
>
<!-- Schermata Selezione Metodo -->
<q-card-section v-if="!metodoSelezionato">
<div class="text-center q-mb-lg">
<div class="text-h6 text-grey-8 q-mb-xs">Come vuoi invitare?</div>
</div>
<div class="selection-buttons">
<q-btn
@click="selezionaMetodo('email')"
class="selection-btn"
unelevated
no-caps
>
<div class="selection-btn-content">
<q-icon
name="email"
size="48px"
color="primary"
/>
<div class="text-h6 q-mt-xs text-grey-9">Email</div>
<div class="text-caption text-grey-7">
Invia un invito diretto via email
</div>
</div>
</q-btn>
<q-btn
@click="selezionaMetodo('telegram')"
class="selection-btn"
unelevated
no-caps
>
<div class="selection-btn-content">
<q-icon
name="telegram"
size="48px"
color="blue-9"
/>
<div class="text-h6 q-mt-md text-grey-9">Telegram</div>
<div class="text-caption text-grey-7">
Condividi tramite Telegram
</div>
</div>
</q-btn>
</div>
</q-card-section>
<!-- Sezione Email (mostrata solo se selezionata) -->
<q-card-section v-if="metodoSelezionato === 'email'">
<div class="q-mb-md">
<q-btn
flat
dense
icon="arrow_back"
label="Cambia metodo"
color="grey-7"
size="sm"
@click="tornaAllaScelta"
/>
</div>
<q-form @submit="onInviaEmail">
<!-- Email Input -->
<q-input
v-model="form.email"
type="email"
label="Email del tuo amico *"
hint="Inserisci l'indirizzo email della persona che vuoi invitare"
label="Email della tua persona amica *"
lazy-rules
:rules="[
(val) => !!val || 'L\'email è obbligatoria',
@@ -86,10 +117,9 @@
<q-input
v-model="form.messaggio"
type="textarea"
label="Messaggio personalizzato (opzionale)"
hint="Aggiungi un messaggio personale al tuo invito"
label="Messaggio personale (opzionale)"
outlined
rows="3"
:rows="tools.isMobile() ? 6 : 9"
counter
maxlength="500"
:disable="invitaStore.loading"
@@ -113,17 +143,6 @@
<q-icon name="message" />
</template>
</q-input>
<!-- Info che viene salvato -->
<div
v-if="form.messaggio"
class="text-caption text-grey-6"
>
<q-icon
name="info"
size="xs"
/>
Questo messaggio sarà riutilizzato nei prossimi inviti
</div>
<!-- Alert errore -->
<q-banner
@@ -145,23 +164,31 @@
icon="email"
color="primary"
size="lg"
class="full-width"
class="full-width q-mt-md"
outline
:loading="invitaStore.loading"
:disable="invitaStore.loading || !form.email"
unelevated
/>
</q-form>
</q-card-section>
<q-separator inset />
<!-- Sezione Telegram -->
<q-card-section>
<!-- Sezione Telegram (mostrata solo se selezionata) -->
<q-card-section v-if="metodoSelezionato === 'telegram'">
<div class="q-mb-md">
<q-btn
dense
icon="arrow_back"
label="Cambia metodo"
size="sm"
outline
@click="tornaAllaScelta"
/>
</div>
<div class="text-center q-mb-md">
<div class="text-subtitle1 text-grey-8 q-mb-xs">
Oppure invita tramite Telegram
Invita tramite Telegram
</div>
<div class="text-caption text-grey-6">
<div class="text-caption text-grey-7">
Genera un messaggio da condividere su Telegram
</div>
</div>
@@ -178,8 +205,11 @@
/>
</q-card-section>
<!-- Info Section -->
<q-card-section class="bg-blue-1">
<!-- Info Section (solo per Telegram) -->
<q-card-section
v-if="metodoSelezionato === 'telegram'"
class="bg-blue-1"
>
<div class="text-center">
<q-icon
name="info"
@@ -188,7 +218,8 @@
class="q-mr-xs"
/>
<span class="text-grey-8">
Riceverai sul {{ tools.getBotName() }} il messaggio da inoltrare alla persona amica.
Riceverai sul {{ tools.getBotName() }} il messaggio da inoltrare alla
persona amica.
</span>
</div>
</q-card-section>
@@ -266,6 +297,29 @@
</div>
</q-card-section>
<!-- Stats Badge -->
<div
v-if="invitaStore.totaleInviti > 0"
class="stats-badge q-mt-md"
>
<q-chip
color="white"
text-color="primary"
icon="email"
class="q-mx-xs"
>
{{ invitaStore.contatoreInvitiRiusciti }} inviati
</q-chip>
<!--<q-chip
v-if="invitaStore.percentualeSuccesso > 0"
color="white"
text-color="primary"
icon="trending_up"
class="q-mx-xs"
>
{{ invitaStore.percentualeSuccesso }}% successo
</q-chip>-->
</div>
<!-- Bottone per mostrare cronologia -->
<q-card-section v-if="invitaStore.hasCronologia && !mostraCronologia">
<q-btn

View File

@@ -9,7 +9,7 @@
Invita un Amico
</div>
<div class="text-subtitle2">
Condividi la nostra app con i tuoi amici!
Condividi la app con i tuoi amici!
</div>
</q-card-section>

View File

@@ -376,7 +376,7 @@
v-if="!tools.isLogged()"
class="text-user text-italic bg-red"
>
{{ t('user.loggati') }}
{{ t('user.non_loggato') }}
</div>
<div