🚀 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 triggers

1.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 toast

1.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:courses

2.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

  1. Usar Server Components por defecto (menos JS al cliente)
  2. shadcn/ui para UI (copiar-pegar, no customizar)
  3. No hacer mobile-first (desktop primero, mobile si alcanza)
  4. Fake data para reviews (no esperar tutorías reales)

🚨 Posibles Problemas

ProblemaSolución
RLS bloquea queriesUsar service_role key temporalmente
Gemini API lentoAgregar loading state
Deploy fallaRevisar env vars en Vercel
Types de SupabaseRegenerar 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! 🚀