diff --git a/app/components/BugReportInput.vue b/app/components/BugReportInput.vue new file mode 100644 index 0000000..67f3bcf --- /dev/null +++ b/app/components/BugReportInput.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/composables/useDevMode.ts b/app/composables/useDevMode.ts new file mode 100644 index 0000000..5bd7909 --- /dev/null +++ b/app/composables/useDevMode.ts @@ -0,0 +1,33 @@ +const STORAGE_KEY = 'entrelineas_dev_unlocked' + +export function useDevMode() { + const config = useRuntimeConfig() + const devKey = config.public.entrelineasDevKey as string + + const unlocked = ref(false) + + if (import.meta.client) { + unlocked.value = localStorage.getItem(STORAGE_KEY) === 'true' + } + + function unlock(key: string): boolean { + if (!devKey) return false + if (key === devKey) { + unlocked.value = true + if (import.meta.client) { + localStorage.setItem(STORAGE_KEY, 'true') + } + return true + } + return false + } + + function lock() { + unlocked.value = false + if (import.meta.client) { + localStorage.removeItem(STORAGE_KEY) + } + } + + return { unlocked, unlock, lock } +} diff --git a/app/composables/useRecaptcha.ts b/app/composables/useRecaptcha.ts new file mode 100644 index 0000000..6e8ffdb --- /dev/null +++ b/app/composables/useRecaptcha.ts @@ -0,0 +1,45 @@ +let loaded = false +let loading = false +let loadPromise: Promise | null = null + +function loadScript(siteKey: string): Promise { + if (loaded) return Promise.resolve() + if (loading && loadPromise) return loadPromise + + loading = true + loadPromise = new Promise((resolve, reject) => { + const script = document.createElement('script') + script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}` + script.async = true + script.defer = true + script.onload = () => { + loaded = true + loading = false + resolve() + } + script.onerror = () => { + loading = false + reject(new Error('Failed to load reCAPTCHA')) + } + document.head.appendChild(script) + }) + + return loadPromise +} + +export function useRecaptcha(siteKey: string) { + async function executeRecaptcha(action: string = 'submit'): Promise { + await loadScript(siteKey) + + return new Promise((resolve, reject) => { + window.grecaptcha.ready(() => { + window.grecaptcha + .execute(siteKey, { action }) + .then((token: string) => resolve(token)) + .catch(reject) + }) + }) + } + + return { executeRecaptcha } +} diff --git a/app/layouts/default.vue b/app/layouts/default.vue index 886f744..1348ea6 100755 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -64,6 +64,12 @@ const links = computed(() => { icon: 'i-lucide-settings', to: '/configuracion', onSelect: () => { open.value = false } + }, + { + label: t('feedback.title'), + icon: 'i-lucide-bug', + to: '/feedback', + onSelect: () => { open.value = false } } ] satisfies NavigationMenuItem[] }) diff --git a/app/pages/changelog.vue b/app/pages/changelog.vue index 58c83a2..871bdca 100644 --- a/app/pages/changelog.vue +++ b/app/pages/changelog.vue @@ -16,6 +16,20 @@ interface Release { } const releases: Release[] = [ + { + version: '0.6', + date: '31 de mayo, 2026', + title: 'Feedback con traducciones, bloqueo de Entrelíneas y acceso desarrollador', + changes: [ + { type: 'nuevo', text: 'Página de Feedback con traducciones completas en 4 idiomas' }, + { type: 'nuevo', text: 'Sistema de bloqueo por clave de desarrollador para secciones en desarrollo' }, + { type: 'nuevo', text: 'Acceso de desarrollador en Configuración con desbloqueo por clave' }, + { type: 'nuevo', text: 'Banner visual mejorado para Entrelíneas cuando está bloqueado' }, + { type: 'mejora', text: 'Traducciones añadidas al componente BugReportInput' }, + { type: 'mejora', text: 'Nombre del tab Feedback ahora usa traducciones' }, + { type: 'mejora', text: 'Textos del banner de Entrelíneas traducidos a 4 idiomas' } + ] + }, { version: '0.5', date: '30 de mayo, 2026', diff --git a/app/pages/configuracion.vue b/app/pages/configuracion.vue index 718ebe1..b8a52d2 100644 --- a/app/pages/configuracion.vue +++ b/app/pages/configuracion.vue @@ -25,6 +25,19 @@ const paginationItems = [ description: t('settings.numbered_desc') } ] + +const { unlocked, unlock, lock } = useDevMode() +const devKeyInput = ref('') +const devKeyError = ref('') + +function tryUnlock() { + devKeyError.value = '' + if (unlock(devKeyInput.value)) { + devKeyInput.value = '' + } else { + devKeyError.value = t('settings.dev_wrong_key') + } +}