@icarusmx/creta 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/bin/creta.js +8 -1576
  2. package/codex-refactor.txt +13 -0
  3. package/lib/builders/LessonBuilder.js +388 -0
  4. package/lib/builders/MenuBuilder.js +154 -0
  5. package/lib/builders/ProjectBuilder.js +56 -0
  6. package/lib/cli/index.js +85 -0
  7. package/lib/commands/help.js +5 -0
  8. package/lib/constants/paths.js +9 -0
  9. package/lib/data/enunciados.js +44 -0
  10. package/lib/data/lessons/index.js +25 -0
  11. package/lib/data/lessons/lesson1-system-decomposition.js +251 -0
  12. package/lib/data/lessons/lesson2-object-requests.js +323 -0
  13. package/lib/data/lessons/lesson3-only-way.js +354 -0
  14. package/lib/data/lessons/lesson4-operation-signatures.js +337 -0
  15. package/lib/data/lessons/lesson5-interface-set.js +346 -0
  16. package/lib/data/lessons/lesson6-interface-design.js +412 -0
  17. package/lib/data/lessons/lesson7-object-definition.js +380 -0
  18. package/lib/data/lessons/sintaxis/terminal-basico.js +50 -0
  19. package/lib/data/menus.js +43 -0
  20. package/lib/data/messages.js +28 -0
  21. package/lib/executors/enunciados-executor.js +73 -0
  22. package/lib/executors/portfolio-executor.js +167 -0
  23. package/lib/executors/proyectos-executor.js +23 -0
  24. package/lib/executors/sintaxis-executor.js +18 -0
  25. package/lib/templates/LevelModifier.js +287 -0
  26. package/lib/utils/file-utils.js +18 -0
  27. package/lib/utils/greeting.js +32 -0
  28. package/lib/utils/input.js +15 -0
  29. package/lib/utils/output.js +4 -0
  30. package/lib/utils/user-state.js +115 -0
  31. package/package.json +4 -1
  32. package/refactor.txt +581 -0
  33. package/test/enunciados.test.js +72 -0
  34. package/lessons/lesson1-system-decomposition.js +0 -313
  35. package/lessons/lesson2-object-requests.js +0 -309
  36. package/lessons/lesson3-only-way.js +0 -324
  37. package/lessons/lesson4-operation-signatures.js +0 -319
  38. package/lessons/lesson5-interface-set.js +0 -326
  39. package/lessons/lesson6-interface-design.js +0 -391
  40. package/lessons/lesson7-object-definition.js +0 -300
@@ -0,0 +1,167 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { MenuBuilder } from '../builders/MenuBuilder.js'
4
+ import { ProjectBuilder } from '../builders/ProjectBuilder.js'
5
+ import { PORTFOLIO_MENU_CONFIG } from '../data/menus.js'
6
+ import { createPromptInterface, askQuestion } from '../utils/input.js'
7
+ import { SVELTEKIT_PORTFOLIO_TEMPLATE_PATH } from '../constants/paths.js'
8
+ import { LevelModifier } from '../templates/LevelModifier.js'
9
+
10
+ const DEFAULT_STUDENT_NAME = 'Tu nombre'
11
+
12
+ function getLevelDescription(level) {
13
+ const descriptions = {
14
+ 1: 'navbar',
15
+ 2: 'navbar + hero',
16
+ 3: 'todo el portafolio'
17
+ }
18
+ return descriptions[level] || 'reto completo'
19
+ }
20
+
21
+ function slugifyName(name) {
22
+ return name.trim().toLowerCase().replace(/\s+/g, '-') || 'creta-portafolio'
23
+ }
24
+
25
+ function deriveStudentName(packageJsonPath) {
26
+ try {
27
+ const raw = fs.readFileSync(packageJsonPath, 'utf8')
28
+ const data = JSON.parse(raw)
29
+ if (typeof data.name === 'string' && data.name.includes('-portafolio')) {
30
+ return data.name
31
+ .replace('-portafolio', '')
32
+ .split('-')
33
+ .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
34
+ .join(' ')
35
+ }
36
+ } catch (error) {
37
+ // ignore and fall back to default name
38
+ }
39
+ return DEFAULT_STUDENT_NAME
40
+ }
41
+
42
+ function isInCretaProject() {
43
+ let cwd
44
+ try {
45
+ cwd = process.cwd()
46
+ } catch (error) {
47
+ console.error('Error: No se puede acceder al directorio actual. Asegúrate de estar en un directorio válido.')
48
+ return false
49
+ }
50
+
51
+ const packageJsonPath = path.join(cwd, 'package.json')
52
+ const layoutPath = path.join(cwd, 'src/routes/+layout.svelte')
53
+ const pagePath = path.join(cwd, 'src/routes/+page.svelte')
54
+
55
+ if (!fs.existsSync(packageJsonPath) || !fs.existsSync(layoutPath) || !fs.existsSync(pagePath)) {
56
+ return false
57
+ }
58
+
59
+ try {
60
+ const layoutContent = fs.readFileSync(layoutPath, 'utf8')
61
+ return layoutContent.includes('RETO CRETA') || layoutContent.includes('🧭 RETO 1: NAVBAR')
62
+ } catch (error) {
63
+ return false
64
+ }
65
+ }
66
+
67
+ async function promptStudentName() {
68
+ const rl = createPromptInterface()
69
+ const name = await askQuestion(rl, '¿Cuál es tu nombre? ') || DEFAULT_STUDENT_NAME
70
+ rl.close()
71
+ return name
72
+ }
73
+
74
+ function applyLevelToFile(filePath, level, studentName) {
75
+ if (!fs.existsSync(filePath)) {
76
+ return false
77
+ }
78
+
79
+ const modifier = new LevelModifier(level)
80
+ let content = fs.readFileSync(filePath, 'utf8')
81
+ content = modifier.apply(content, path.basename(filePath))
82
+
83
+ const projectSlug = slugifyName(studentName)
84
+ content = content.replace(/\{\{STUDENT_NAME\}\}/g, studentName)
85
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, `${projectSlug}-portafolio`)
86
+
87
+ fs.writeFileSync(filePath, content)
88
+ return true
89
+ }
90
+
91
+ async function createPortfolioProject(level) {
92
+ const name = await promptStudentName()
93
+ const projectSlug = slugifyName(name)
94
+ const projectName = `${projectSlug}-portafolio`
95
+ const targetPath = path.join(process.cwd(), projectName)
96
+
97
+ console.log(`\n🚀 Creando proyecto: ${projectName}`)
98
+ console.log(`📚 Nivel: ${level === 0 ? 'Reto completo' : `Con ${getLevelDescription(level)} completado`}`)
99
+
100
+ const builder = new ProjectBuilder(SVELTEKIT_PORTFOLIO_TEMPLATE_PATH)
101
+ builder.createProject(targetPath, projectName, name, level)
102
+
103
+ console.log('\n✨ ¡Proyecto creado exitosamente!')
104
+ console.log(`\n📁 Ingresa a tu proyecto: cd ${projectName}`)
105
+ console.log('📦 Instala dependencias: npm install')
106
+ console.log('🚀 Inicia el servidor: npm run dev')
107
+ console.log('\n💡 Lee los comentarios en los archivos para saber qué hacer')
108
+ }
109
+
110
+ async function unlockExistingProject(level) {
111
+ console.log(`\n🔓 ¡Te ayudo a desbloquear el nivel ${level}!`)
112
+ console.log(`📚 Agregando código para: ${getLevelDescription(level)}`)
113
+
114
+ let cwd
115
+ try {
116
+ cwd = process.cwd()
117
+ } catch (error) {
118
+ throw new Error('No se puede acceder al directorio actual. Asegúrate de estar en un directorio válido.')
119
+ }
120
+
121
+ const packageJsonPath = path.join(cwd, 'package.json')
122
+ const studentName = deriveStudentName(packageJsonPath)
123
+
124
+ const layoutPath = path.join(cwd, 'src/routes/+layout.svelte')
125
+ const pagePath = path.join(cwd, 'src/routes/+page.svelte')
126
+
127
+ if (applyLevelToFile(layoutPath, level, studentName)) {
128
+ console.log('✅ Actualizado: src/routes/+layout.svelte')
129
+ }
130
+
131
+ if (applyLevelToFile(pagePath, level, studentName)) {
132
+ console.log('✅ Actualizado: src/routes/+page.svelte')
133
+ }
134
+
135
+ console.log(`\n🎉 ¡Listo! Ahora tienes el código del nivel ${level}`)
136
+ console.log('💡 Revisa los archivos actualizados para ver qué se agregó')
137
+ console.log('🚀 Continúa con: npm run dev')
138
+ }
139
+
140
+ async function runPortfolioFlow(level) {
141
+ try {
142
+ if (level > 0 && isInCretaProject()) {
143
+ await unlockExistingProject(level)
144
+ } else {
145
+ await createPortfolioProject(level)
146
+ }
147
+ } catch (error) {
148
+ console.error('Error al preparar el portafolio:', error.message)
149
+ throw error
150
+ }
151
+ }
152
+
153
+ export async function executePortfolio(level = 0) {
154
+ await runPortfolioFlow(level)
155
+ }
156
+
157
+ export async function executePortfolioMenu() {
158
+ const menu = new MenuBuilder(PORTFOLIO_MENU_CONFIG)
159
+ const choice = await menu.show()
160
+
161
+ if (!choice) {
162
+ return
163
+ }
164
+
165
+ const level = typeof choice.level === 'number' ? choice.level : choice.id || 0
166
+ await runPortfolioFlow(level)
167
+ }
@@ -0,0 +1,23 @@
1
+ import { MenuBuilder } from '../builders/MenuBuilder.js'
2
+ import { PROYECTOS_MENU_CONFIG } from '../data/menus.js'
3
+ import { PullRequestTutorial } from '../pr-tutorial.js'
4
+ import { executePortfolioMenu } from './portfolio-executor.js'
5
+
6
+ export async function executeProyectos() {
7
+ const menu = new MenuBuilder(PROYECTOS_MENU_CONFIG)
8
+ const choice = await menu.show()
9
+
10
+ if (!choice) {
11
+ return
12
+ }
13
+
14
+ if (choice.id === 'pr') {
15
+ const tutorial = new PullRequestTutorial()
16
+ await tutorial.start()
17
+ return
18
+ }
19
+
20
+ if (choice.id === 'portfolio') {
21
+ await executePortfolioMenu()
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import { LessonBuilder } from '../builders/LessonBuilder.js'
2
+ import { TERMINAL_BASICO } from '../data/lessons/sintaxis/terminal-basico.js'
3
+ import { clearConsole } from '../utils/output.js'
4
+
5
+ export async function executeSintaxis() {
6
+ // Show selection confirmation
7
+ console.log('\nElegiste: Aprender sintaxis')
8
+ console.log('Cargando...')
9
+
10
+ await new Promise(resolve => setTimeout(resolve, 1000))
11
+
12
+ // Clear screen but keep scroll history
13
+ clearConsole()
14
+
15
+ // Start lesson
16
+ const lesson = new LessonBuilder(TERMINAL_BASICO)
17
+ await lesson.start()
18
+ }
@@ -0,0 +1,287 @@
1
+ export class LevelModifier {
2
+ constructor(level) {
3
+ this.level = level
4
+ }
5
+
6
+ apply(content, filename) {
7
+ if (this.level === 0) {
8
+ return content
9
+ }
10
+
11
+ if (filename === '+layout.svelte') {
12
+ return this.applyLayoutModifications(content)
13
+ }
14
+
15
+ if (filename === '+page.svelte') {
16
+ return this.applyPageModifications(content)
17
+ }
18
+
19
+ return content
20
+ }
21
+
22
+ applyLayoutModifications(content) {
23
+ if (this.level >= 1) {
24
+ content = content.replace(
25
+ '<nav class="bg-white shadow-sm">\n <!-- ⚠️ COMPLETA AQUÍ LA NAVEGACIÓN -->\n</nav>',
26
+ `<nav class="bg-white shadow-sm">
27
+ <div class="max-w-7xl mx-auto px-4">
28
+ <div class="flex justify-between items-center py-3">
29
+ <div class="flex items-center space-x-3">
30
+ <img src="https://icarus.mx/logo.png" alt="Logo" class="w-8 h-8">
31
+ <span class="text-xl font-bold text-gray-900">{{STUDENT_NAME}}</span>
32
+ </div>
33
+ <div class="hidden md:flex space-x-6">
34
+ <a href="#inicio" class="text-gray-600 hover:text-blue-600">Inicio</a>
35
+ <a href="#sobre-mi" class="text-gray-600 hover:text-blue-600">Sobre mí</a>
36
+ <a href="#proyectos" class="text-gray-600 hover:text-blue-600">Proyectos</a>
37
+ <a href="#contacto" class="text-gray-600 hover:text-blue-600">Contacto</a>
38
+ </div>
39
+ <button class="md:hidden">
40
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
41
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
42
+ </svg>
43
+ </button>
44
+ </div>
45
+ </div>
46
+ </nav>`
47
+ )
48
+ }
49
+
50
+ if (this.level >= 3) {
51
+ content = content.replace(
52
+ '<footer class="bg-gray-50 border-t">\n <!-- ⚠️ COMPLETA AQUÍ EL FOOTER -->\n</footer>',
53
+ `<footer class="bg-gray-50 border-t">
54
+ <div class="max-w-7xl mx-auto py-8 px-4">
55
+ <div class="text-center text-gray-600 text-sm space-y-2">
56
+ <p>© ${new Date().getFullYear()} {{STUDENT_NAME}}. Todos los derechos reservados.</p>
57
+ <p>Hecho con ❤️ en <a href="https://creta.school" class="text-blue-600 hover:underline">Creta</a></p>
58
+ </div>
59
+ </div>
60
+ </footer>`
61
+ )
62
+ }
63
+
64
+ return content
65
+ }
66
+
67
+ applyPageModifications(content) {
68
+ if (this.level >= 2) {
69
+ content = content.replace(
70
+ `<section class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
71
+ <!-- ⚠️ COMPLETA AQUÍ LA SECCIÓN HERO -->
72
+ <div class="max-w-4xl mx-auto px-4 text-center">
73
+ <h1 class="text-4xl md:text-6xl font-bold text-gray-900 mb-6">
74
+ Gracias por seguir aquí
75
+ </h1>
76
+ <p class="text-lg text-gray-600 mb-8">
77
+ Abre el proyecto usando <code>code .</code> y sigue las instrucciones. Si tienes dudas, apóyate en el equipo
78
+ </p>
79
+ </div>
80
+ </section>`,
81
+ `<section id="inicio" class="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
82
+ <div class="max-w-4xl mx-auto px-4 text-center">
83
+ <div class="animate-fade-in">
84
+ <h1 class="text-4xl md:text-6xl font-bold text-gray-900 mb-6">
85
+ Hola, soy {{STUDENT_NAME}}
86
+ </h1>
87
+ <p class="text-lg text-gray-600 mb-8 max-w-2xl mx-auto">
88
+ Soy un desarrollador apasionado por crear productos digitales que impacten positivamente.
89
+ Aprendo construyendo y siempre busco nuevos desafíos.
90
+ </p>
91
+ <div class="flex justify-center space-x-4">
92
+ <a href="#proyectos" class="bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg transition-colors duration-200 font-medium">
93
+ Ver proyectos
94
+ </a>
95
+ <a href="#contacto" class="border border-blue-600 text-blue-600 hover:bg-blue-50 px-8 py-3 rounded-lg transition-colors duration-200 font-medium">
96
+ Contáctame
97
+ </a>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ </section>`
102
+ )
103
+ }
104
+
105
+ if (this.level >= 3) {
106
+ content = content.replace(
107
+ `<!-- ⚠️ AQUÍ VAN LAS DEMÁS SECCIONES (SOBRE MÍ, PROYECTOS, CONTACTO) -->
108
+ <!-- Revisa los comentarios arriba para saber qué crear -->`,
109
+ `<!-- Sobre mí -->
110
+ <section id="sobre-mi" class="py-20 bg-white">
111
+ <div class="max-w-4xl mx-auto px-4">
112
+ <div class="text-center mb-16">
113
+ <h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6">Sobre mí</h2>
114
+ <div class="w-20 h-1 bg-blue-600 mx-auto mb-8"></div>
115
+ </div>
116
+
117
+ <div class="grid md:grid-cols-2 gap-12 items-center">
118
+ <div>
119
+ <div class="bg-gray-100 aspect-square rounded-lg flex items-center justify-center mb-6 md:mb-0">
120
+ <span class="text-gray-500 text-lg">Tu foto aquí</span>
121
+ </div>
122
+ </div>
123
+ <div>
124
+ <p class="text-lg text-gray-600 mb-6 leading-relaxed">
125
+ Soy un desarrollador apasionado por crear productos digitales que impacten positivamente.
126
+ Aprendo construyendo y siempre busco nuevos desafíos que me permitan crecer profesionalmente.
127
+ </p>
128
+ <div class="grid grid-cols-2 gap-4">
129
+ <div class="bg-blue-50 p-4 rounded-lg text-center">
130
+ <div class="text-2xl font-bold text-blue-600">2+</div>
131
+ <div class="text-sm text-gray-600">Años aprendiendo</div>
132
+ </div>
133
+ <div class="bg-green-50 p-4 rounded-lg text-center">
134
+ <div class="text-2xl font-bold text-green-600">5+</div>
135
+ <div class="text-sm text-gray-600">Proyectos</div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ </section>
142
+
143
+ <!-- Proyectos -->
144
+ <section id="proyectos" class="py-20 bg-gray-50">
145
+ <div class="max-w-6xl mx-auto px-4">
146
+ <div class="text-center mb-16">
147
+ <h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6">Mis Proyectos</h2>
148
+ <div class="w-20 h-1 bg-blue-600 mx-auto mb-8"></div>
149
+ <p class="text-lg text-gray-600 max-w-2xl mx-auto">
150
+ Algunos de los proyectos en los que he trabajado y que demuestran mis habilidades
151
+ </p>
152
+ </div>
153
+
154
+ <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
155
+ <!-- Proyecto 1 -->
156
+ <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200 project-card">
157
+ <div class="bg-gradient-to-br from-blue-400 to-blue-600 h-48 flex items-center justify-center">
158
+ <span class="text-white text-lg font-medium">Proyecto 1</span>
159
+ </div>
160
+ <div class="p-6">
161
+ <h3 class="text-xl font-semibold text-gray-900 mb-3">Nombre del Proyecto</h3>
162
+ <p class="text-gray-600 mb-4">Descripción breve del proyecto y las tecnologías utilizadas.</p>
163
+ <div class="flex space-x-3">
164
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver código</a>
165
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver demo</a>
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Proyecto 2 -->
171
+ <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200 project-card">
172
+ <div class="bg-gradient-to-br from-green-400 to-green-600 h-48 flex items-center justify-center">
173
+ <span class="text-white text-lg font-medium">Proyecto 2</span>
174
+ </div>
175
+ <div class="p-6">
176
+ <h3 class="text-xl font-semibold text-gray-900 mb-3">Nombre del Proyecto</h3>
177
+ <p class="text-gray-600 mb-4">Descripción breve del proyecto y las tecnologías utilizadas.</p>
178
+ <div class="flex space-x-3">
179
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver código</a>
180
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver demo</a>
181
+ </div>
182
+ </div>
183
+ </div>
184
+
185
+ <!-- Proyecto 3 -->
186
+ <div class="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-200 project-card">
187
+ <div class="bg-gradient-to-br from-purple-400 to-purple-600 h-48 flex items-center justify-center">
188
+ <span class="text-white text-lg font-medium">Proyecto 3</span>
189
+ </div>
190
+ <div class="p-6">
191
+ <h3 class="text-xl font-semibold text-gray-900 mb-3">Nombre del Proyecto</h3>
192
+ <p class="text-gray-600 mb-4">Descripción breve del proyecto y las tecnologías utilizadas.</p>
193
+ <div class="flex space-x-3">
194
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver código</a>
195
+ <a href="#" class="text-blue-600 hover:text-blue-700 text-sm font-medium">Ver demo</a>
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </section>
202
+
203
+ <!-- Contacto -->
204
+ <section id="contacto" class="py-20 bg-white">
205
+ <div class="max-w-4xl mx-auto px-4">
206
+ <div class="text-center mb-16">
207
+ <h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-6">Contacto</h2>
208
+ <div class="w-20 h-1 bg-blue-600 mx-auto mb-8"></div>
209
+ <p class="text-lg text-gray-600 max-w-2xl mx-auto">
210
+ ¿Tienes un proyecto en mente? ¡Hablemos y veamos cómo puedo ayudarte!
211
+ </p>
212
+ </div>
213
+
214
+ <div class="grid md:grid-cols-2 gap-12">
215
+ <div>
216
+ <h3 class="text-xl font-semibold text-gray-900 mb-6">Información de contacto</h3>
217
+ <div class="space-y-4">
218
+ <div class="flex items-center space-x-3">
219
+ <div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
220
+ <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
221
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
222
+ </svg>
223
+ </div>
224
+ <div>
225
+ <p class="text-gray-900 font-medium">Email</p>
226
+ <p class="text-gray-600">tu.email@ejemplo.com</p>
227
+ </div>
228
+ </div>
229
+
230
+ <div class="flex items-center space-x-3">
231
+ <div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
232
+ <svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
233
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
234
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
235
+ </svg>
236
+ </div>
237
+ <div>
238
+ <p class="text-gray-900 font-medium">Ubicación</p>
239
+ <p class="text-gray-600">Tu ciudad, País</p>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <div class="mt-8">
245
+ <h4 class="text-lg font-medium text-gray-900 mb-4">Sígueme</h4>
246
+ <div class="flex space-x-4">
247
+ <a href="#" class="w-10 h-10 bg-gray-100 hover:bg-blue-100 rounded-lg flex items-center justify-center transition-colors duration-200">
248
+ <span class="text-gray-600 hover:text-blue-600">LI</span>
249
+ </a>
250
+ <a href="#" class="w-10 h-10 bg-gray-100 hover:bg-blue-100 rounded-lg flex items-center justify-center transition-colors duration-200">
251
+ <span class="text-gray-600 hover:text-blue-600">GH</span>
252
+ </a>
253
+ <a href="#" class="w-10 h-10 bg-gray-100 hover:bg-blue-100 rounded-lg flex items-center justify-center transition-colors duration-200">
254
+ <span class="text-gray-600 hover:text-blue-600">TW</span>
255
+ </a>
256
+ </div>
257
+ </div>
258
+ </div>
259
+
260
+ <div>
261
+ <form class="space-y-6">
262
+ <div>
263
+ <label for="name" class="block text-sm font-medium text-gray-700 mb-2">Nombre</label>
264
+ <input type="text" id="name" name="name" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent">
265
+ </div>
266
+ <div>
267
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">Email</label>
268
+ <input type="email" id="email" name="email" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent">
269
+ </div>
270
+ <div>
271
+ <label for="message" class="block text-sm font-medium text-gray-700 mb-2">Mensaje</label>
272
+ <textarea id="message" name="message" rows="4" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"></textarea>
273
+ </div>
274
+ <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-lg transition-colors duration-200 font-medium">
275
+ Enviar mensaje
276
+ </button>
277
+ </form>
278
+ </div>
279
+ </div>
280
+ </div>
281
+ </section>`
282
+ )
283
+ }
284
+
285
+ return content
286
+ }
287
+ }
@@ -0,0 +1,18 @@
1
+ import path from 'path'
2
+
3
+ export const BINARY_EXTENSIONS = new Set([
4
+ '.png',
5
+ '.jpg',
6
+ '.jpeg',
7
+ '.gif',
8
+ '.svg',
9
+ '.ico',
10
+ '.woff',
11
+ '.woff2',
12
+ '.ttf',
13
+ '.eot'
14
+ ])
15
+
16
+ export function isBinaryFile(filePath) {
17
+ return BINARY_EXTENSIONS.has(path.extname(filePath).toLowerCase())
18
+ }
@@ -0,0 +1,32 @@
1
+ import chalk from 'chalk'
2
+ import { UserState } from './user-state.js'
3
+ import { askQuestion, createPromptInterface } from './input.js'
4
+
5
+ export async function greetUser() {
6
+ const state = UserState.updateLastSeen()
7
+
8
+ // First time user - ask for name
9
+ if (!state.name) {
10
+ const rl = createPromptInterface()
11
+
12
+ console.log('')
13
+ const name = await askQuestion(rl, chalk.cyan('👋 ¿Cómo te llamas? '))
14
+
15
+ if (name && name.trim()) {
16
+ UserState.setName(name.trim())
17
+ console.log(chalk.green(`\n¡Bienvenido, ${name.trim()}!`))
18
+ }
19
+
20
+ rl.close()
21
+ console.log('')
22
+ return
23
+ }
24
+
25
+ // Returning user - show personalized greeting
26
+ const lessonsCount = state.lessonsCompleted.length
27
+ const greeting = lessonsCount > 0
28
+ ? `¡Hola de vuelta, ${state.name}! 📊 ${lessonsCount} ${lessonsCount === 1 ? 'lección completada' : 'lecciones completadas'}`
29
+ : `¡Hola de vuelta, ${state.name}!`
30
+
31
+ console.log(chalk.cyan(`\n${greeting}\n`))
32
+ }
@@ -0,0 +1,15 @@
1
+ import { createInterface } from 'readline'
2
+
3
+ export function createPromptInterface(options = {}) {
4
+ return createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ ...options
8
+ })
9
+ }
10
+
11
+ export function askQuestion(rl, question) {
12
+ return new Promise((resolve) => {
13
+ rl.question(question, resolve)
14
+ })
15
+ }
@@ -0,0 +1,4 @@
1
+ export function clearConsole() {
2
+ process.stdout.write('\x1b[2J')
3
+ process.stdout.write('\x1b[H')
4
+ }
@@ -0,0 +1,115 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ const STATE_DIR = path.join(os.homedir(), '.creta')
6
+ const STATE_FILE = path.join(STATE_DIR, 'user.json')
7
+
8
+ export class UserState {
9
+ static load() {
10
+ try {
11
+ if (!fs.existsSync(STATE_FILE)) {
12
+ return this.createDefault()
13
+ }
14
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'))
15
+ } catch {
16
+ return this.createDefault()
17
+ }
18
+ }
19
+
20
+ static save(data) {
21
+ try {
22
+ if (!fs.existsSync(STATE_DIR)) {
23
+ fs.mkdirSync(STATE_DIR, { recursive: true })
24
+ }
25
+ fs.writeFileSync(STATE_FILE, JSON.stringify(data, null, 2))
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ static createDefault() {
33
+ return {
34
+ name: null,
35
+ createdAt: new Date().toISOString(),
36
+ lessonsCompleted: [],
37
+ projectsCreated: [],
38
+ discord: null,
39
+ stats: {
40
+ totalSessions: 0,
41
+ lastSeen: null
42
+ }
43
+ }
44
+ }
45
+
46
+ static updateLastSeen() {
47
+ const state = this.load()
48
+ state.stats.lastSeen = new Date().toISOString()
49
+ state.stats.totalSessions += 1
50
+ this.save(state)
51
+ return state
52
+ }
53
+
54
+ static completLesson(lessonId) {
55
+ const state = this.load()
56
+
57
+ // Check if already completed
58
+ const alreadyCompleted = state.lessonsCompleted.find(l => l.id === lessonId)
59
+ if (alreadyCompleted) return state
60
+
61
+ state.lessonsCompleted.push({
62
+ id: lessonId,
63
+ completedAt: new Date().toISOString()
64
+ })
65
+
66
+ this.save(state)
67
+ return state
68
+ }
69
+
70
+ static addProject(projectName, projectType, level = 0) {
71
+ const state = this.load()
72
+
73
+ state.projectsCreated.push({
74
+ name: projectName,
75
+ type: projectType,
76
+ level,
77
+ createdAt: new Date().toISOString()
78
+ })
79
+
80
+ this.save(state)
81
+ return state
82
+ }
83
+
84
+ static setName(name) {
85
+ const state = this.load()
86
+ state.name = name
87
+ this.save(state)
88
+ return state
89
+ }
90
+
91
+ static getPath() {
92
+ return STATE_FILE
93
+ }
94
+
95
+ static hasName() {
96
+ const state = this.load()
97
+ return state.name !== null && state.name.trim() !== ''
98
+ }
99
+
100
+ static getName() {
101
+ const state = this.load()
102
+ return state.name
103
+ }
104
+
105
+ static getStats() {
106
+ const state = this.load()
107
+ return {
108
+ name: state.name,
109
+ lessonsCompleted: state.lessonsCompleted.length,
110
+ projectsCreated: state.projectsCreated.length,
111
+ totalSessions: state.stats.totalSessions,
112
+ lastSeen: state.stats.lastSeen
113
+ }
114
+ }
115
+ }