@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.
- package/bin/creta.js +8 -1576
- package/codex-refactor.txt +13 -0
- package/lib/builders/LessonBuilder.js +388 -0
- package/lib/builders/MenuBuilder.js +154 -0
- package/lib/builders/ProjectBuilder.js +56 -0
- package/lib/cli/index.js +85 -0
- package/lib/commands/help.js +5 -0
- package/lib/constants/paths.js +9 -0
- package/lib/data/enunciados.js +44 -0
- package/lib/data/lessons/index.js +25 -0
- package/lib/data/lessons/lesson1-system-decomposition.js +251 -0
- package/lib/data/lessons/lesson2-object-requests.js +323 -0
- package/lib/data/lessons/lesson3-only-way.js +354 -0
- package/lib/data/lessons/lesson4-operation-signatures.js +337 -0
- package/lib/data/lessons/lesson5-interface-set.js +346 -0
- package/lib/data/lessons/lesson6-interface-design.js +412 -0
- package/lib/data/lessons/lesson7-object-definition.js +380 -0
- package/lib/data/lessons/sintaxis/terminal-basico.js +50 -0
- package/lib/data/menus.js +43 -0
- package/lib/data/messages.js +28 -0
- package/lib/executors/enunciados-executor.js +73 -0
- package/lib/executors/portfolio-executor.js +167 -0
- package/lib/executors/proyectos-executor.js +23 -0
- package/lib/executors/sintaxis-executor.js +18 -0
- package/lib/templates/LevelModifier.js +287 -0
- package/lib/utils/file-utils.js +18 -0
- package/lib/utils/greeting.js +32 -0
- package/lib/utils/input.js +15 -0
- package/lib/utils/output.js +4 -0
- package/lib/utils/user-state.js +115 -0
- package/package.json +4 -1
- package/refactor.txt +581 -0
- package/test/enunciados.test.js +72 -0
- package/lessons/lesson1-system-decomposition.js +0 -313
- package/lessons/lesson2-object-requests.js +0 -309
- package/lessons/lesson3-only-way.js +0 -324
- package/lessons/lesson4-operation-signatures.js +0 -319
- package/lessons/lesson5-interface-set.js +0 -326
- package/lessons/lesson6-interface-design.js +0 -391
- 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,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
|
+
}
|