many fixes and improvements - rework for modules/ and common/

feat(emailer): add Postmark and Resend providers
This commit is contained in:
2025-06-06 12:21:43 -04:00
parent 31ba18fa8d
commit 25b94d3e02
313 changed files with 6586 additions and 18260 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@
import { ref } from 'vue';
import { useAuthStore } from '@/stores/authStore.js';
import { useI18n } from 'vue-i18n';
import { mdiEye, mdiEyeOff } from '@mdi/js';
const { t } = useI18n();
const authStore = useAuthStore();

View File

@@ -8,12 +8,24 @@
:label="t('email')"
type="email"
variant="outlined"
:error-messages="emailErrors"
:rules="emailRules"
validate-on="blur"
/>
<v-alert
v-if="!!errorMessage"
outlined
type="error"
class="mt-4">
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button class="secondary" @click="$emit('closeRequested')">
{{ t('cancel') }}
</button>
<button class="primary" @click="saveEmail" :disabled="isLoading">
<button class="primary" @click="saveEmail" :disabled="!canSave || isLoading">
{{ t('save') }}
</button>
</div>
@@ -22,7 +34,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
@@ -40,6 +52,34 @@ const props = defineProps({
const email = ref(props.creator.presentation?.email || '');
const isLoading = ref(false);
const errorMessage = ref('');
// Email validation
const isValidEmail = (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const emailRules = [
v => !!v || t('validation.emailRequired'),
v => isValidEmail(v) || t('validation.emailInvalid'),
];
const emailErrors = computed(() => {
if (!email.value) {
return [t('validation.emailRequired')];
}
if (!isValidEmail(email.value)) {
return [t('validation.emailInvalid')];
}
return [];
});
const canSave = computed(() => {
return email.value &&
isValidEmail(email.value) &&
email.value !== (props.creator.presentation?.email || '');
});
async function saveEmail() {
if (!props.creator.id) {
@@ -47,24 +87,34 @@ async function saveEmail() {
return;
}
if (!canSave.value) {
return;
}
try {
isLoading.value = true;
errorMessage.value = '';
// Save email
await client.post(
`/api/creators/${props.creator.id}/email`,
{
email: email.value || ""
email: email.value.trim()
}
);
// Refresh creator profile
await creatorProfileStore.fetchCreatorProfile();
// Close dialog
emit('closeRequested');
} catch (error) {
console.error("Error saving email:", error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
}
} finally {
isLoading.value = false;
}
@@ -85,19 +135,40 @@ const emit = defineEmits(['closeRequested']);
"changeEmail": "Change Email",
"email": "Email",
"save": "Save",
"cancel": "Cancel"
"cancel": "Cancel",
"validation": {
"emailRequired": "Email is required",
"emailInvalid": "Please enter a valid email address"
},
"errors": {
"unexpected": "An unexpected error occurred"
}
},
"fr": {
"changeEmail": "Modifier l'email",
"email": "Email",
"save": "Enregistrer",
"cancel": "Annuler"
"cancel": "Annuler",
"validation": {
"emailRequired": "L'email est requis",
"emailInvalid": "Veuillez entrer une adresse email valide"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
},
"es": {
"changeEmail": "Cambiar correo electrónico",
"email": "Correo electrónico",
"save": "Guardar",
"cancel": "Cancelar"
"cancel": "Cancelar",
"validation": {
"emailRequired": "El correo electrónico es obligatorio",
"emailInvalid": "Por favor ingrese una dirección de correo electrónico válida"
},
"errors": {
"unexpected": "Se produjo un error inesperado"
}
}
}
</i18n>
</i18n>

View File

@@ -3,17 +3,33 @@
<div class="card-title">{{ t('changePhoneNumber') }}</div>
<div class="card-content">
<v-text-field
v-model="phoneNumber"
v-model="displayPhoneNumber"
class="w-full p-2"
:label="t('phoneNumber')"
type="tel"
variant="outlined"
:error-messages="phoneErrors"
:rules="phoneRules"
validate-on="blur"
:placeholder="t('phonePlaceholder')"
@input="handlePhoneInput"
@keydown="handleKeydown"
maxlength="14"
/>
<v-alert
v-if="!!errorMessage"
outlined
type="error"
class="mt-4">
{{ errorMessage }}
</v-alert>
<div class="card-actions">
<button class="secondary" @click="$emit('closeRequested')">
{{ t('cancel') }}
</button>
<button class="primary" @click="savePhoneNumber" :disabled="isLoading">
<button class="primary" @click="savePhoneNumber" :disabled="!canSave || isLoading">
{{ t('save') }}
</button>
</div>
@@ -22,7 +38,7 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useClient } from '@/plugins/api.js';
import { useCreatorProfileStore } from '@/stores/creatorProfileStore.js';
@@ -38,8 +54,99 @@ const props = defineProps({
}
});
const phoneNumber = ref(props.creator.presentation?.phoneNumber || '');
// Format existing phone number to display format
const formatPhoneForDisplay = (phone) => {
if (!phone) return '';
const digits = phone.replace(/\D/g, '');
if (digits.length === 10) {
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
}
return phone;
};
// Extract just the digits from formatted phone
const extractDigits = (formattedPhone) => {
return formattedPhone.replace(/\D/g, '');
};
const displayPhoneNumber = ref(formatPhoneForDisplay(props.creator.presentation?.phoneNumber || ''));
const phoneDigits = ref(extractDigits(displayPhoneNumber.value));
const isLoading = ref(false);
const errorMessage = ref('');
// Phone number formatting and validation
const formatPhoneNumber = (digits) => {
// Remove all non-digits
const cleaned = digits.replace(/\D/g, '');
// Apply formatting based on length
if (cleaned.length === 0) return '';
if (cleaned.length <= 3) return `(${cleaned}`;
if (cleaned.length <= 6) return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3)}`;
return `(${cleaned.slice(0, 3)}) ${cleaned.slice(3, 6)}-${cleaned.slice(6, 10)}`;
};
const handlePhoneInput = (event) => {
const input = event.target.value;
const digits = extractDigits(input);
// Limit to 10 digits
if (digits.length > 10) return;
phoneDigits.value = digits;
displayPhoneNumber.value = formatPhoneNumber(digits);
};
const handleKeydown = (event) => {
// Allow backspace, delete, tab, escape, enter
if ([8, 9, 27, 13, 46].includes(event.keyCode)) return;
// Allow Ctrl+A, Ctrl+C, Ctrl+V, Ctrl+X
if ((event.ctrlKey || event.metaKey) && [65, 67, 86, 88].includes(event.keyCode)) return;
// Allow arrow keys
if (event.keyCode >= 35 && event.keyCode <= 40) return;
// Only allow numbers (0-9)
if (event.keyCode < 48 || event.keyCode > 57) {
event.preventDefault();
}
};
// Watch for changes to phoneDigits to update display
watch(phoneDigits, (newDigits) => {
displayPhoneNumber.value = formatPhoneNumber(newDigits);
});
const isValidPhoneNumber = (digits) => {
return digits.length === 10;
};
const phoneRules = [
v => {
const digits = extractDigits(v);
return digits.length > 0 || t('validation.phoneRequired');
},
v => {
const digits = extractDigits(v);
return isValidPhoneNumber(digits) || t('validation.phoneInvalid');
},
];
const phoneErrors = computed(() => {
if (phoneDigits.value.length === 0) {
return [t('validation.phoneRequired')];
}
if (!isValidPhoneNumber(phoneDigits.value)) {
return [t('validation.phoneInvalid')];
}
return [];
});
const canSave = computed(() => {
return phoneDigits.value.length === 10 &&
phoneDigits.value !== extractDigits(props.creator.presentation?.phoneNumber || '');
});
async function savePhoneNumber() {
if (!props.creator.id) {
@@ -47,14 +154,21 @@ async function savePhoneNumber() {
return;
}
if (!canSave.value) {
return;
}
try {
isLoading.value = true;
errorMessage.value = '';
// Save phone number
// Save the formatted phone number
const formattedPhone = formatPhoneNumber(phoneDigits.value);
await client.post(
`/api/creators/${props.creator.id}/phone`,
{
phoneNumber: phoneNumber.value || ""
phoneNumber: formattedPhone
}
);
@@ -65,6 +179,11 @@ async function savePhoneNumber() {
emit('closeRequested');
} catch (error) {
console.error("Error saving phone number:", error);
if (error?.response?.data?.errors) {
errorMessage.value = error.response.data.errors[0]?.['reason'] || t('errors.unexpected');
} else {
errorMessage.value = error?.response?.data?.message || error.message || t('errors.unexpected');
}
} finally {
isLoading.value = false;
}
@@ -84,20 +203,44 @@ const emit = defineEmits(['closeRequested']);
"en": {
"changePhoneNumber": "Change Phone Number",
"phoneNumber": "Phone Number",
"phonePlaceholder": "(555) 123-4567",
"save": "Save",
"cancel": "Cancel"
"cancel": "Cancel",
"validation": {
"phoneRequired": "Phone number is required",
"phoneInvalid": "Please enter a complete 10-digit phone number"
},
"errors": {
"unexpected": "An unexpected error occurred"
}
},
"fr": {
"changePhoneNumber": "Modifier le numéro de téléphone",
"phoneNumber": "Numéro de téléphone",
"phonePlaceholder": "(555) 123-4567",
"save": "Enregistrer",
"cancel": "Annuler"
"cancel": "Annuler",
"validation": {
"phoneRequired": "Le numéro de téléphone est requis",
"phoneInvalid": "Veuillez entrer un numéro de téléphone complet à 10 chiffres"
},
"errors": {
"unexpected": "Une erreur inattendue s'est produite"
}
},
"es": {
"changePhoneNumber": "Cambiar número de teléfono",
"phoneNumber": "Número de teléfono",
"phonePlaceholder": "(555) 123-4567",
"save": "Guardar",
"cancel": "Cancelar"
"cancel": "Cancelar",
"validation": {
"phoneRequired": "El número de teléfono es obligatorio",
"phoneInvalid": "Por favor ingrese un número de teléfono completo de 10 dígitos"
},
"errors": {
"unexpected": "Se produjo un error inesperado"
}
}
}
</i18n>
</i18n>