🚀 TutorConnect UC - Plan de Desarrollo Completo (Supabase)
📋 Resumen Ejecutivo
Objetivo: Plataforma P2P de tutorías UC con marketplace de ofertas/búsquedas + Gemini AI
Tiempo: 9 horas (19 diciembre, 9:00-18:00)
Stack: Next.js 14 + Supabase + TypeScript + shadcn/ui + Gemini AI
Deploy: Vercel (todo en uno)
🎯 Estrategia General
✅ Enfoque “MVP Primero, Polish Después”
Horas 1-6: Features core funcionando (aunque feas)
Horas 7-8: Gemini AI + Polish
Hora 9: Testing + Deploy + Video
🚫 NO Hacer Durante el Hackathon
- ❌ Autenticación OAuth (solo email/password)
- ❌ Sistema de pagos
- ❌ Chat en tiempo real
- ❌ Notificaciones push
- ❌ Tests unitarios
- ❌ Animaciones complejas
⏱️ Timeline Detallado
🔧 Hora 1 (9:00-10:00): Infrastructure Setup
1.1 Supabase Setup (15 min)
# Ir a supabase.com/dashboard
1. Crear proyecto "tutorconnect-uc"
2. Región: South America (São Paulo)
3. Copiar:
- Project URL
- anon public key
- service_role key (secret)SQL Editor en Supabase:
-- Ejecutar schema completo (de Base de Datos Supabase.md)
-- 1. Crear ENUMs
-- 2. Crear tablas
-- 3. Crear índices
-- 4. Crear RLS policies
-- 5. Crear funciones
-- 6. Crear triggers1.2 Next.js Project Setup (20 min)
# Crear proyecto
npx create-next-app@latest tutorconnect-uc
# ✓ TypeScript
# ✓ ESLint
# ✓ Tailwind CSS
# ✓ App Router
# ✓ No usar src/ directory
# ✓ Import alias: @/*
cd tutorconnect-uc
# Instalar dependencias
npm install @supabase/supabase-js @supabase/auth-helpers-nextjs
npm install zod react-hook-form @hookform/resolvers
npm install date-fns clsx tailwind-merge
npm install class-variance-authority
npm install lucide-react
# shadcn/ui init
npx shadcn-ui@latest init
# ✓ Default style
# ✓ Base color: Slate
# ✓ CSS variables: Yes
# Instalar componentes shadcn
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add input
npx shadcn-ui@latest add form
npx shadcn-ui@latest add select
npx shadcn-ui@latest add badge
npx shadcn-ui@latest add avatar
npx shadcn-ui@latest add tabs
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add toast1.3 Configurar Supabase Client (10 min)
// lib/supabase/client.ts
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
import { Database } from '@/types/database.types'
export const supabase = createClientComponentClient<Database>()// lib/supabase/server.ts
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
import { Database } from '@/types/database.types'
export const createServerClient = () =>
createServerComponentClient<Database>({ cookies }).env.local:
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx...
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
NEXT_PUBLIC_GEMINI_API_KEY=AIzaxx...1.4 Deploy Inicial (15 min)
# Conectar con GitHub
git init
git add .
git commit -m "Initial setup"
git remote add origin https://github.com/tu-user/tutorconnect-uc.git
git push -u origin main
# Deploy en Vercel
# 1. Ir a vercel.com
# 2. Import from GitHub
# 3. Agregar env vars
# 4. Deploy✅ Checkpoint Hora 1: Proyecto desplegado + BD creada
📊 Hora 2 (10:00-11:00): Seed Data + Auth
2.1 Seed Ramos (20 min)
// scripts/seed-courses.ts
import { createClient } from '@supabase/supabase-js'
import fs from 'fs'
import readline from 'readline'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
async function seedCourses() {
const fileStream = fs.createReadStream('data/courses.ndjson')
const rl = readline.createInterface({ input: fileStream })
const courses = []
for await (const line of rl) {
const course = JSON.parse(line)
courses.push({
codigo: course.code,
nombre: course.name,
descripcion: course.description || null
})
// Batch insert cada 100
if (courses.length >= 100) {
const { error } = await supabase.from('ramos').insert(courses)
if (error) console.error('Error:', error)
console.log(`Inserted ${courses.length} courses`)
courses.length = 0
}
}
// Insert restantes
if (courses.length > 0) {
await supabase.from('ramos').insert(courses)
console.log(`Final batch: ${courses.length} courses`)
}
console.log('✅ All courses seeded!')
}
seedCourses()# Ejecutar seed
npm run seed:courses2.2 Crear Tutores Fake (15 min)
// scripts/seed-tutors.ts
import { createClient } from '@supabase/supabase-js'
import { faker } from '@faker-js/faker'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
async function seedTutors() {
const campuses = ['San Joaquín', 'Casa Central', 'Lo Contador']
const careers = [
'Ingeniería Civil en Computación',
'Ingeniería Comercial',
'Medicina',
'Derecho',
'Psicología'
]
for (let i = 0; i < 20; i++) {
// 1. Crear auth user
const email = `tutor${i}@uc.cl`
const { data: authData, error: authError } = await supabase.auth.admin.createUser({
email,
password: 'password123',
email_confirm: true,
user_metadata: {
full_name: faker.person.fullName()
}
})
if (authError || !authData.user) continue
// 2. Crear profile
await supabase.from('profiles').insert({
id: authData.user.id,
full_name: authData.user.user_metadata.full_name,
email,
role: 'tutor'
})
// 3. Crear tutor
const { data: tutorData } = await supabase.from('tutores').insert({
user_id: authData.user.id,
bio: faker.lorem.paragraph(),
campus: faker.helpers.arrayElement(campuses),
carrera: faker.helpers.arrayElement(careers),
año_ingreso: faker.number.int({ min: 2018, max: 2022 }),
tarifa: faker.number.int({ min: 10000, max: 25000 }),
disponibilidad: {
lunes: ['15:00-17:00'],
miercoles: ['10:00-12:00']
},
auto_accept: faker.datatype.boolean()
}).select().single()
// 4. Asignar ramos aleatorios
const { data: randomRamos } = await supabase
.from('ramos')
.select('id')
.limit(3)
.order('random()')
if (randomRamos) {
for (const ramo of randomRamos) {
await supabase.from('tutor_ramo').insert({
tutor_id: tutorData.id,
ramo_id: ramo.id,
nota: Number((Math.random() * 2 + 5).toFixed(1)), // 5.0-7.0
semestre: faker.helpers.arrayElement(['2023-1', '2023-2', '2024-1'])
})
}
}
console.log(`✅ Tutor ${i + 1}/20 created`)
}
}
seedTutors()2.3 Implementar Auth (25 min)
// app/auth/login/page.tsx
'use client'
import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase/client'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
export default function LoginPage() {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
if (!email.endsWith('@uc.cl')) {
setError('Debes usar tu email institucional @uc.cl')
return
}
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
setError(error.message)
} else {
router.push('/')
router.refresh()
}
}
return (
<div className="container max-w-md mx-auto py-20">
<h1 className="text-3xl font-bold mb-8">Iniciar Sesión</h1>
<form onSubmit={handleLogin} className="space-y-4">
<Input
type="email"
placeholder="tu.email@uc.cl"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input
type="password"
placeholder="Contraseña"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && <p className="text-red-500 text-sm">{error}</p>}
<Button type="submit" className="w-full">
Iniciar Sesión
</Button>
</form>
</div>
)
}// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
const res = NextResponse.next()
const supabase = createMiddlewareClient({ req, res })
const { data: { session } } = await supabase.auth.getSession()
// Rutas protegidas
if (!session && req.nextUrl.pathname.startsWith('/tutor')) {
return NextResponse.redirect(new URL('/auth/login', req.url))
}
return res
}
export const config = {
matcher: ['/tutor/:path*', '/mis-tutorias/:path*']
}✅ Checkpoint Hora 2: Auth funcional + BD con datos
🏠 Hora 3 (11:00-12:00): Landing + Búsqueda
3.1 Landing Page (30 min)
// app/page.tsx
import { createServerClient } from '@/lib/supabase/server'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import Link from 'next/link'
export default async function Home() {
const supabase = createServerClient()
// Featured tutors
const { data: tutores } = await supabase
.from('tutores_con_stats')
.select('*')
.order('avg_rating', { ascending: false })
.limit(6)
return (
<div>
{/* Hero */}
<section className="py-20 bg-gradient-to-b from-blue-50">
<div className="container max-w-4xl mx-auto text-center">
<h1 className="text-5xl font-bold mb-6">
Encuentra el mejor tutor UC para aprobar con 7.0
</h1>
<p className="text-xl text-gray-600 mb-8">
Tutores verificados de tu misma universidad.
Con la nota con la que aprobaron.
</p>
<form action="/buscar" className="flex gap-2 max-w-xl mx-auto">
<Input
name="q"
placeholder="Busca tu ramo: IIC2233, MAT1610..."
className="text-lg"
/>
<Button type="submit" size="lg">Buscar</Button>
</form>
</div>
</section>
{/* Cómo funciona */}
<section className="py-16">
<div className="container max-w-5xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">
Cómo Funciona
</h2>
<div className="grid md:grid-cols-3 gap-8">
<div className="text-center">
<div className="text-4xl mb-4">🔍</div>
<h3 className="font-bold mb-2">1. Busca tu ramo</h3>
<p className="text-gray-600">
Filtra por código, campus y precio
</p>
</div>
<div className="text-center">
<div className="text-4xl mb-4">✅</div>
<h3 className="font-bold mb-2">2. Elige tutor verificado</h3>
<p className="text-gray-600">
Ve su nota, reviews y disponibilidad
</p>
</div>
<div className="text-center">
<div className="text-4xl mb-4">📅</div>
<h3 className="font-bold mb-2">3. Agenda y aprueba</h3>
<p className="text-gray-600">
Coordina presencial u online
</p>
</div>
</div>
</div>
</section>
{/* Tutores destacados */}
<section className="py-16 bg-gray-50">
<div className="container max-w-6xl mx-auto">
<h2 className="text-3xl font-bold text-center mb-12">
Tutores Destacados
</h2>
<div className="grid md:grid-cols-3 gap-6">
{tutores?.map((tutor) => (
<TutorCard key={tutor.id} tutor={tutor} />
))}
</div>
<div className="text-center mt-8">
<Button asChild variant="outline">
<Link href="/buscar">Ver todos los tutores →</Link>
</Button>
</div>
</div>
</section>
</div>
)
}3.2 Página de Búsqueda (30 min)
// app/buscar/page.tsx
import { createServerClient } from '@/lib/supabase/server'
import SearchFilters from '@/components/search-filters'
import TutorGrid from '@/components/tutor-grid'
type SearchParams = {
q?: string
campus?: string
min_price?: string
max_price?: string
}
export default async function BuscarPage({
searchParams
}: {
searchParams: SearchParams
}) {
const supabase = createServerClient()
let query = supabase
.from('tutores_con_stats')
.select('*')
// Filtro por ramo (código)
if (searchParams.q) {
query = query.contains('ramo_codigos', [searchParams.q.toUpperCase()])
}
// Filtro por campus
if (searchParams.campus) {
query = query.eq('campus', searchParams.campus)
}
// Filtro por precio
if (searchParams.min_price) {
query = query.gte('tarifa', parseInt(searchParams.min_price))
}
if (searchParams.max_price) {
query = query.lte('tarifa', parseInt(searchParams.max_price))
}
const { data: tutores, error } = await query
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Buscar Tutores</h1>
<div className="grid md:grid-cols-4 gap-6">
<aside className="md:col-span-1">
<SearchFilters />
</aside>
<main className="md:col-span-3">
<TutorGrid tutores={tutores || []} />
</main>
</div>
</div>
)
}✅ Checkpoint Hora 3: Landing + búsqueda funcional
👤 Hora 4 (12:00-13:00): Perfil de Tutor
// app/tutor/[id]/page.tsx
import { createServerClient } from '@/lib/supabase/server'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
export default async function TutorProfilePage({
params
}: {
params: { id: string }
}) {
const supabase = createServerClient()
const { data: tutor } = await supabase
.from('tutores_con_stats')
.select('*')
.eq('id', params.id)
.single()
const { data: ramos } = await supabase
.from('tutor_ramo')
.select('*, ramos(*)')
.eq('tutor_id', params.id)
const { data: reviews } = await supabase
.from('reviews')
.select('*, profiles!student_id(*)')
.eq('tutor_id', params.id)
.order('created_at', { ascending: false })
.limit(10)
return (
<div className="container max-w-4xl mx-auto py-8">
{/* Header */}
<div className="flex gap-6 mb-8">
<Avatar className="h-24 w-24">
<AvatarFallback>{tutor.full_name[0]}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h1 className="text-3xl font-bold">{tutor.full_name}</h1>
<div className="flex items-center gap-2 text-gray-600 mt-2">
<span>⭐ {tutor.avg_rating}</span>
<span>•</span>
<span>{tutor.total_reviews} reviews</span>
<span>•</span>
<span>{tutor.carrera}</span>
</div>
<Badge className="mt-2">{tutor.campus}</Badge>
</div>
<div className="text-right">
<p className="text-2xl font-bold">${tutor.tarifa.toLocaleString()}</p>
<p className="text-sm text-gray-600">por módulo</p>
</div>
</div>
{/* Tabs */}
<Tabs defaultValue="sobre-mi">
<TabsList>
<TabsTrigger value="sobre-mi">Sobre mí</TabsTrigger>
<TabsTrigger value="ramos">Ramos</TabsTrigger>
<TabsTrigger value="reviews">Reviews</TabsTrigger>
</TabsList>
<TabsContent value="sobre-mi" className="mt-6">
<p className="text-gray-700 whitespace-pre-line">{tutor.bio}</p>
</TabsContent>
<TabsContent value="ramos" className="mt-6">
<div className="space-y-4">
{ramos?.map((tr) => (
<div key={tr.id} className="border p-4 rounded-lg">
<div className="flex justify-between items-start">
<div>
<h3 className="font-bold">{tr.ramos.codigo}</h3>
<p className="text-gray-600">{tr.ramos.nombre}</p>
</div>
<Badge>Nota: {tr.nota}</Badge>
</div>
<Button className="mt-4" size="sm">
Agendar tutoría →
</Button>
</div>
))}
</div>
</TabsContent>
<TabsContent value="reviews" className="mt-6">
<div className="space-y-4">
{reviews?.map((review) => (
<div key={review.id} className="border p-4 rounded-lg">
<div className="flex justify-between mb-2">
<span className="font-semibold">
{review.profiles.full_name}
</span>
<span>{'⭐'.repeat(review.rating)}</span>
</div>
<p className="text-gray-700">{review.comment}</p>
</div>
))}
</div>
</TabsContent>
</Tabs>
</div>
)
}✅ Checkpoint Hora 4: Perfil completo funcionando
💼 Hora 5-6 (13:00-15:00): Marketplace + Sessions
(Código similar al de UX-UI Design.md pero adaptado a Supabase)
Features a implementar:
- Ofertas de tutores (feed)
- Búsquedas de estudiantes (feed)
- Agendar tutoría (form)
- Dashboard del tutor (stats básicos)
- Mis tutorías (lista)
✅ Checkpoint Hora 6: Marketplace completo
🤖 Hora 7 (15:00-16:00): Gemini AI
// supabase/functions/gemini-match/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
import { GoogleGenerativeAI } from 'https://esm.sh/@google/generative-ai@0.1.0'
serve(async (req) => {
const { courseCode, preferences } = await req.json()
// 1. Fetch tutors for this course
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { data: tutors } = await supabase
.from('tutores_con_stats')
.select('*')
.contains('ramo_codigos', [courseCode])
// 2. Call Gemini
const genAI = new GoogleGenerativeAI(Deno.env.get('GEMINI_API_KEY')!)
const model = genAI.getGenerativeModel({ model: 'gemini-pro' })
const prompt = `
Eres un experto en matching de tutores para estudiantes universitarios.
Analiza estos tutores y recomienda los mejores 3 para un estudiante que busca ayuda en ${courseCode}.
Tutores disponibles:
${JSON.stringify(tutors, null, 2)}
Preferencias del estudiante:
${JSON.stringify(preferences, null, 2)}
Responde SOLO con un JSON válido en este formato:
[
{
"tutorId": "uuid",
"score": 95,
"reasoning": "Explicación breve de por qué este tutor es ideal"
}
]
`
const result = await model.generateContent(prompt)
const text = result.response.text()
const recommendations = JSON.parse(text.replace(/```json\n?/g, '').replace(/```\n?/g, ''))
return new Response(JSON.stringify(recommendations), {
headers: { 'Content-Type': 'application/json' }
})
})Deploy Edge Function:
supabase functions deploy gemini-match✅ Checkpoint Hora 7: Gemini AI funcionando
✨ Hora 8 (16:00-17:00): Polish
- Reviews system
- UI improvements
- Mobile responsive
- Loading states
- Error handling
🧪 Hora 9 (17:00-18:00): Testing + Deploy
- Manual testing
- Bug fixes
- Final deploy
- Video demo (2-3 min)
📝 Notas Importantes
⚡ Atajos para Ganar Tiempo
- Usar Server Components por defecto (menos JS al cliente)
- shadcn/ui para UI (copiar-pegar, no customizar)
- No hacer mobile-first (desktop primero, mobile si alcanza)
- Fake data para reviews (no esperar tutorías reales)
🚨 Posibles Problemas
| Problema | Solución |
|---|---|
| RLS bloquea queries | Usar service_role key temporalmente |
| Gemini API lento | Agregar loading state |
| Deploy falla | Revisar env vars en Vercel |
| Types de Supabase | Regenerar con supabase gen types |
✅ Checklist Final
Pre-Hackathon
- Cuenta Supabase creada
- Cuenta Vercel creada
- Gemini API key obtenida
- courses.ndjson descargado
- Este documento impreso/accesible
Durante Hackathon
- Hora 1: ✅ Setup completo
- Hora 2: ✅ Data seeded + Auth
- Hora 3: ✅ Landing + Búsqueda
- Hora 4: ✅ Perfil tutor
- Hora 5-6: ✅ Marketplace
- Hora 7: ✅ Gemini AI
- Hora 8: ✅ Polish
- Hora 9: ✅ Deploy final
¡Todo listo para el hackathon! 🚀