feat: initial brainstorming app — Nuxt 3 + SQLite + admin auth

Nuxt 3 app with:
- SQLite (better-sqlite3) for persistence
- Anonymous idea submission and voting
- Admin auth with session cookies
- AI analysis via Gemini API
- Nuxt UI components + Tailwind

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Martinez
2026-04-07 14:15:45 +02:00
commit e7de636cf2
25 changed files with 9114 additions and 0 deletions

143
app/pages/admin.vue Normal file
View File

@@ -0,0 +1,143 @@
<script setup lang="ts">
const { admin, logout } = useAuth()
const toast = useToast()
const analysisKey = ref('')
const analysis = ref<any>(null)
const analyzing = ref(false)
const { data: ideas, refresh } = useFetch<any[]>('/api/ideas')
watch(admin, (v) => {
if (!v) navigateTo('/login')
}, { immediate: true })
async function deleteIdea(id: number) {
await $fetch(`/api/ideas/${id}`, { method: 'DELETE' })
toast.add({ title: 'Idea eliminada', color: 'success' })
refresh()
}
async function toggleHide(idea: any) {
await $fetch(`/api/ideas/${idea.id}`, {
method: 'PATCH',
body: { hidden: !idea.hidden },
})
refresh()
}
async function analyze() {
if (!analysisKey.value) {
toast.add({ title: 'Introduce la API key de Gemini', color: 'warning' })
return
}
analyzing.value = true
try {
analysis.value = await $fetch('/api/analysis', {
method: 'POST',
body: { apiKey: analysisKey.value },
})
} catch (e: any) {
toast.add({ title: e.data?.message || 'Error en análisis', color: 'error' })
} finally {
analyzing.value = false
}
}
async function doLogout() {
await logout()
navigateTo('/')
}
</script>
<template>
<div class="min-h-screen bg-neutral-50 p-4 md:p-8">
<header class="max-w-5xl mx-auto flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold">Panel de Administración</h1>
<p class="text-neutral-500 text-sm">Gestiona ideas y genera análisis IA.</p>
</div>
<div class="flex gap-2">
<NuxtLink to="/">
<UButton variant="ghost">Ver tablón</UButton>
</NuxtLink>
<UButton color="error" variant="soft" @click="doLogout">Cerrar sesión</UButton>
</div>
</header>
<main class="max-w-5xl mx-auto grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- AI Analysis -->
<div class="lg:col-span-1 space-y-4">
<UCard>
<template #header>
<h2 class="font-semibold">Análisis IA</h2>
</template>
<div class="space-y-3">
<UFormField label="Gemini API Key">
<UInput v-model="analysisKey" type="password" placeholder="AIza..." />
</UFormField>
<UButton block :loading="analyzing" :disabled="analyzing" @click="analyze">
Generar Análisis
</UButton>
</div>
</UCard>
<UCard v-if="analysis">
<template #header>
<h3 class="font-semibold">Resultados</h3>
</template>
<div class="space-y-4 text-sm">
<div>
<h4 class="font-bold text-primary mb-1">Fricción Principal</h4>
<ul class="space-y-1 text-neutral-600">
<li v-for="(p, i) in analysis.topFrictionPoints" :key="i"> {{ p }}</li>
</ul>
</div>
<div>
<h4 class="font-bold text-amber-600 mb-1">Quick Wins</h4>
<ul class="space-y-1 text-neutral-600">
<li v-for="(w, i) in analysis.quickWins" :key="i"> {{ w }}</li>
</ul>
</div>
<div>
<h4 class="font-bold text-neutral-500 mb-1">Visión Estratégica</h4>
<p class="text-neutral-500 italic">{{ analysis.strategicAdvice }}</p>
</div>
</div>
</UCard>
</div>
<!-- Ideas management -->
<div class="lg:col-span-2">
<h2 class="text-lg font-bold mb-4">Todas las ideas ({{ ideas?.length ?? 0 }})</h2>
<div class="space-y-3">
<UCard v-for="idea in ideas" :key="idea.id" :class="{ 'opacity-50': idea.hidden }">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<UBadge color="primary" variant="subtle" size="xs">{{ idea.category }}</UBadge>
<UBadge v-if="idea.hidden" color="warning" variant="subtle" size="xs">Oculta</UBadge>
<span class="text-xs text-neutral-400">{{ new Date(idea.created_at).toLocaleString() }}</span>
<UBadge color="neutral" size="xs">{{ idea.votes }} votos</UBadge>
</div>
<p class="text-sm">{{ idea.text }}</p>
</div>
<div class="flex gap-1 ml-4 shrink-0">
<UButton
variant="ghost"
size="xs"
:color="idea.hidden ? 'success' : 'warning'"
@click="toggleHide(idea)"
>
{{ idea.hidden ? 'Mostrar' : 'Ocultar' }}
</UButton>
<UButton variant="ghost" size="xs" color="error" @click="deleteIdea(idea.id)">
Borrar
</UButton>
</div>
</div>
</UCard>
</div>
</div>
</main>
</div>
</template>