- aggiornata la grafica della Home di RISO

- Profilo Completition
- Email Verificata
- Invita un Amico (invio di email)
This commit is contained in:
Surya Paolo
2025-11-15 19:38:39 +01:00
parent d812c6c799
commit b8df3ea721
110 changed files with 10856 additions and 2765 deletions

View File

@@ -0,0 +1,49 @@
.invita-amico-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.invita-amico-card {
max-width: 600px;
width: 100%;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
@media (max-width: $breakpoint-sm-max) {
margin: 0;
border-radius: 0;
}
}
.q-card__section--vert {
padding: 24px;
@media (max-width: $breakpoint-xs-max) {
padding: 16px;
}
}
.stats-badge {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
// Animazioni
.q-btn {
transition: all 0.3s ease;
&:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.q-list {
border-radius: 8px;
overflow: hidden;
}

View File

@@ -0,0 +1,180 @@
import { defineComponent, ref, reactive, onMounted } from 'vue';
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';
// Chiave localStorage
const MESSAGGIO_STORAGE_KEY = 'invita-amico-messaggio-personalizzato';
export default defineComponent({
name: 'InvitaAmico',
emits: ['invito-inviato', 'telegram-click'],
setup(props, { emit }) {
// Composables
const $q = useQuasar();
const invitaStore = useInvitaAmicoStore();
// State
const mostraCronologia = ref(false);
const form = reactive<InvitoAmicoForm & { usernameInvitante?: string }>({
email: '',
messaggio: '',
usernameInvitante: '',
});
// ==========================================
// METHODS
// ==========================================
/**
* Invia invito via email usando lo store Pinia
*/
const onInviaEmail = async () => {
invitaStore.resetStato();
if (form.messaggio) {
localStorage.setItem(MESSAGGIO_STORAGE_KEY, form.messaggio.trim());
}
const result = await invitaStore.inviaInvitoEmail(
tools.getIdApp(),
form.email,
form.messaggio || undefined
);
if (result.success) {
$q.notify({
type: 'positive',
message: 'Invito inviato con successo! 🎉',
caption: `L'email è stata inviata a ${form.email}`,
icon: 'check_circle',
timeout: 3000,
actions: [
{
label: 'Vedi cronologia',
color: 'white',
handler: () => {
mostraCronologia.value = true;
},
},
],
});
const sentEmail = form.email;
form.email = '';
form.usernameInvitante = '';
emit('invito-inviato', result.emailInviata ? sentEmail : '');
} else {
$q.notify({
type: 'negative',
message: "Errore nell'invio dell'invito",
caption: result.message,
icon: 'error',
timeout: 5000,
});
}
};
/**
* Gestione click Telegram
*/
const onInviaTelegram = async () => {
emit('telegram-click');
const success = await invitaStore.inviaInvitoTelegram(form.messaggio);
if (success) {
$q.notify({
type: 'positive',
message: 'Messaggio inviato via Telegram! ✈️',
icon: 'telegram',
timeout: 2000,
});
} else {
$q.notify({
type: 'negative',
message: invitaStore.error || 'Errore invio Telegram',
icon: 'error',
timeout: 3000,
});
}
};
/**
* Conferma eliminazione cronologia
*/
const confermaEliminaCronologia = () => {
$q.dialog({
title: 'Conferma',
message: 'Sei sicuro di voler cancellare tutta la cronologia degli inviti?',
cancel: true,
persistent: true,
}).onOk(() => {
invitaStore.svuotaCronologia();
mostraCronologia.value = false;
$q.notify({
type: 'info',
message: 'Cronologia cancellata',
icon: 'delete',
timeout: 2000,
});
});
};
/**
* Formatta data per visualizzazione
*/
const formatDate = (date: Date): string => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Adesso';
if (minutes < 60) return `${minutes} min fa`;
if (hours < 24) return `${hours} ore fa`;
if (days < 7) return `${days} giorni fa`;
return date.toLocaleDateString('it-IT', {
day: '2-digit',
month: 'short',
hour: '2-digit',
minute: '2-digit',
});
};
// Carica messaggio all'apertura
onMounted(() => {
const salvato = localStorage.getItem(MESSAGGIO_STORAGE_KEY);
if (salvato) {
form.messaggio = salvato;
}
});
// Cancella
const cancellaMessaggioSalvato = () => {
localStorage.removeItem(MESSAGGIO_STORAGE_KEY);
form.messaggio = '';
$q.notify({ type: 'info', message: 'Messaggio cancellato' });
};
// RETURN
return {
mostraCronologia,
form,
onInviaEmail,
onInviaTelegram,
confermaEliminaCronologia,
formatDate,
invitaStore,
cancellaMessaggioSalvato,
};
},
});

View File

@@ -0,0 +1,290 @@
<template>
<q-page class="invita-amico-page">
<div class="q-pa-md">
<q-card class="invita-amico-card">
<!-- Header -->
<q-card-section class="bg-primary text-white text-center">
<div class="text-h5 q-mb-xs">
<q-icon
name="person_add"
size="md"
class="q-mr-sm"
/>
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>
</q-card-section>
<q-separator />
<!-- Form Section -->
<q-card-section>
<q-form
@submit="onInviaEmail"
class="q-gutter-md"
>
<!-- 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"
lazy-rules
:rules="[
(val) => !!val || 'L\'email è obbligatoria',
(val) => invitaStore.isValidEmail(val) || 'Inserisci un\'email valida',
(val) =>
!invitaStore.isEmailGiaInvitata(val) ||
'Email già invitata nelle ultime 24 ore',
]"
outlined
:disable="invitaStore.loading"
>
<template v-slot:prepend>
<q-icon name="email" />
</template>
<!-- Badge se già invitata -->
<template
v-slot:append
v-if="form.email && invitaStore.isEmailGiaInvitata(form.email)"
>
<q-icon
name="info"
color="orange"
>
<q-tooltip>Già invitato nelle ultime 24h</q-tooltip>
</q-icon>
</template>
</q-input>
<!-- Messaggio Personalizzato (opzionale) -->
<q-input
v-model="form.messaggio"
type="textarea"
label="Messaggio personalizzato (opzionale)"
hint="Aggiungi un messaggio personale al tuo invito"
outlined
rows="3"
counter
maxlength="500"
:disable="invitaStore.loading"
>
<!-- Bottone per cancellare messaggio salvato -->
<template
v-slot:append
v-if="form.messaggio"
>
<q-btn
flat
dense
round
icon="clear"
@click.stop="cancellaMessaggioSalvato"
>
<q-tooltip>Cancella messaggio salvato</q-tooltip>
</q-btn>
</template>
<template v-slot:prepend>
<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
v-if="invitaStore.error"
class="bg-negative text-white"
rounded
dense
>
<template v-slot:avatar>
<q-icon name="error" />
</template>
{{ invitaStore.error }}
</q-banner>
<!-- Bottone Invio Email -->
<q-btn
type="submit"
label="Invia Invito via Email"
icon="email"
color="primary"
size="lg"
class="full-width"
:loading="invitaStore.loading"
:disable="invitaStore.loading || !form.email"
unelevated
/>
</q-form>
</q-card-section>
<q-separator inset />
<!-- Sezione Telegram -->
<q-card-section>
<div class="text-center q-mb-md">
<div class="text-subtitle1 text-grey-8 q-mb-xs">
Oppure invita tramite Telegram
</div>
<div class="text-caption text-grey-6">
Genera un messaggio da condividere su Telegram
</div>
</div>
<q-btn
@click="onInviaTelegram"
label="Invia via Telegram"
icon="telegram"
color="blue-9"
size="lg"
class="full-width"
outline
:disable="invitaStore.loading"
/>
</q-card-section>
<!-- Info Section -->
<q-card-section class="bg-blue-1">
<div class="text-center">
<q-icon
name="info"
color="primary"
size="sm"
class="q-mr-xs"
/>
<span class="text-grey-8">
Il tuo amico riceverà un link per registrarsi all'app
</span>
</div>
</q-card-section>
<!-- Cronologia Inviti (opzionale) -->
<q-card-section v-if="invitaStore.hasCronologia && mostraCronologia">
<div class="text-subtitle2 text-grey-8 q-mb-sm">
<q-icon
name="history"
class="q-mr-xs"
/>
Ultimi inviti
<q-btn
flat
dense
round
icon="close"
size="sm"
@click="mostraCronologia = false"
class="float-right"
/>
</div>
<q-list
dense
bordered
separator
>
<q-item
v-for="invito in invitaStore.ultimi5Inviti"
:key="invito.id"
>
<q-item-section avatar>
<q-icon
:name="invito.successo ? 'check_circle' : 'error'"
:color="invito.successo ? 'positive' : 'negative'"
/>
</q-item-section>
<q-item-section>
<q-item-label>{{ invito.email }}</q-item-label>
<q-item-label caption>
{{ formatDate(invito.data) }}
<span
v-if="invito.errore"
class="text-negative"
>
- {{ invito.errore }}
</span>
</q-item-label>
</q-item-section>
<q-item-section side>
<q-btn
flat
dense
round
icon="delete"
size="sm"
@click="invitaStore.rimuoviDaCronologia(invito.id)"
/>
</q-item-section>
</q-item>
</q-list>
<div class="text-center q-mt-sm">
<q-btn
flat
dense
label="Cancella cronologia"
color="negative"
size="sm"
@click="confermaEliminaCronologia"
/>
</div>
</q-card-section>
<!-- Bottone per mostrare cronologia -->
<q-card-section v-if="invitaStore.hasCronologia && !mostraCronologia">
<q-btn
flat
dense
label="Mostra cronologia inviti"
icon="history"
color="primary"
class="full-width"
@click="mostraCronologia = true"
/>
</q-card-section>
</q-card>
</div>
</q-page>
</template>
<script lang="ts" src="./InvitaAmico.ts"></script>
<style lang="scss" scoped>
@import './InvitaAmico.scss';
</style>

View File

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