- aggiornata la grafica della Home di RISO
- Profilo Completition - Email Verificata - Invita un Amico (invio di email)
This commit is contained in:
49
src/components/InvitaAmico/InvitaAmico.scss
Normal file
49
src/components/InvitaAmico/InvitaAmico.scss
Normal 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;
|
||||
}
|
||||
180
src/components/InvitaAmico/InvitaAmico.ts
Normal file
180
src/components/InvitaAmico/InvitaAmico.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
});
|
||||
290
src/components/InvitaAmico/InvitaAmico.vue
Normal file
290
src/components/InvitaAmico/InvitaAmico.vue
Normal 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>
|
||||
1
src/components/InvitaAmico/index.ts
Normal file
1
src/components/InvitaAmico/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {default as InvitaAmico} from './InvitaAmico.vue'
|
||||
Reference in New Issue
Block a user