From c61e0e03ac2c6b3c1bbc3e7a2da6552e810595e6 Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 2 Jun 2025 19:05:50 -0400
Subject: [PATCH 01/15] error de ruta
---
frontend/angular/src/app/app.routes.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/angular/src/app/app.routes.ts b/frontend/angular/src/app/app.routes.ts
index 8d38e40..2b0da4f 100644
--- a/frontend/angular/src/app/app.routes.ts
+++ b/frontend/angular/src/app/app.routes.ts
@@ -12,6 +12,6 @@ export const routes: Routes = [
// listas
{path: "lista-videos", component: VideosListComponent},
{path: "lista-ejercicios", component: ExercisesListComponent},
- {path: "progreso-aprendizaje", component: LearningProgressComponent}
+ {path: "progreso-aprendizaje", component: LearningProgressComponent},
{path: "lista-cursos", component: CoursesListComponent}
];
\ No newline at end of file
From ddbc4f62b68ec71c62f826b5620f8ecd7ce791d8 Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 18:43:22 -0400
Subject: [PATCH 02/15] redirije a cursos al completar o cancelar Crear curso
---
.../src/app/pages/create-course/create-course.component.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.ts b/frontend/angular/src/app/pages/create-course/create-course.component.ts
index 4c0c33f..a1c96eb 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.ts
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.ts
@@ -261,7 +261,7 @@ export class CreateCourseComponent implements OnInit {
this.addContent(); // Agregar contenido inicial
// Opcional: Navegar a la lista de cursos o a ver el curso creado
- // this.router.navigate(['/courses']);
+ this.router.navigate(['/cursos']);
this.isSubmitting = false;
},
@@ -283,6 +283,7 @@ export class CreateCourseComponent implements OnInit {
this.courseForm.reset();
this.contents.clear();
this.addContent(); // Agregar contenido inicial
+ this.router.navigate(['/cursos'])
}
}
From 2b6b2fafb77be3ed4da39ce07455ecb0cfae1324 Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 19:17:45 -0400
Subject: [PATCH 03/15] componente de formulario de curso separado
---
.../form-course/form-course.component.html | 191 +++++++
.../form-course/form-course.component.scss | 511 ++++++++++++++++++
.../form-course/form-course.component.spec.ts | 0
.../form-course/form-course.component.ts | 336 ++++++++++++
.../create-course.component.html | 230 +-------
.../create-course.component.scss | 404 --------------
.../create-course/create-course.component.ts | 314 +++--------
7 files changed, 1131 insertions(+), 855 deletions(-)
create mode 100644 frontend/angular/src/app/components/form-course/form-course.component.html
create mode 100644 frontend/angular/src/app/components/form-course/form-course.component.scss
create mode 100644 frontend/angular/src/app/components/form-course/form-course.component.spec.ts
create mode 100644 frontend/angular/src/app/components/form-course/form-course.component.ts
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.html b/frontend/angular/src/app/components/form-course/form-course.component.html
new file mode 100644
index 0000000..55d521d
--- /dev/null
+++ b/frontend/angular/src/app/components/form-course/form-course.component.html
@@ -0,0 +1,191 @@
+
\ No newline at end of file
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.scss b/frontend/angular/src/app/components/form-course/form-course.component.scss
new file mode 100644
index 0000000..99deedb
--- /dev/null
+++ b/frontend/angular/src/app/components/form-course/form-course.component.scss
@@ -0,0 +1,511 @@
+@use "sass:color";
+
+// Variables de color
+$primary-color: #4346ff;
+$secondary-color: #ff6a81;
+$background-dark: #000000;
+$background-light: #272149;
+$text-light: #ffffff;
+$text-muted: rgba(255, 255, 255, 0.8);
+$card-bg: rgba(255, 255, 255, 0.05);
+$card-border: rgba(255, 255, 255, 0.1);
+$error-color: #f44336;
+$success-color: #4caf50;
+
+.course-form {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 120px;
+
+ @media (max-width: 768px) {
+ padding: 0 20px;
+ }
+}
+
+.form-section {
+ background: $card-bg;
+ border: 1px solid $card-border;
+ border-radius: 12px;
+ padding: 30px;
+ margin-bottom: 30px;
+ backdrop-filter: blur(10px);
+
+ h2 {
+ font-size: 1.5rem;
+ margin-bottom: 25px;
+ font-weight: 600;
+ position: relative;
+ color: $text-light;
+
+ &:after {
+ content: "";
+ position: absolute;
+ bottom: -8px;
+ left: 0;
+ width: 60px;
+ height: 3px;
+ background: linear-gradient(90deg, $primary-color, $secondary-color);
+ border-radius: 2px;
+ }
+ }
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 25px;
+
+ @media (max-width: 576px) {
+ flex-direction: column;
+ gap: 15px;
+ align-items: stretch;
+ }
+}
+
+.form-group {
+ margin-bottom: 20px;
+
+ label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 500;
+ color: $text-light;
+ font-size: 0.95rem;
+ }
+}
+
+.form-input,
+.form-textarea,
+.form-select {
+ width: 100%;
+ padding: 12px 16px;
+ background: rgba(255, 255, 255, 0.1);
+ border: 1px solid $card-border;
+ border-radius: 8px;
+ color: $text-light;
+ font-size: 0.95rem;
+ transition: all 0.3s ease;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.5);
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $secondary-color;
+ box-shadow: 0 0 0 3px rgba(138, 99, 210, 0.1);
+ background: rgba(255, 255, 255, 0.15);
+ }
+
+ &:invalid {
+ border-color: $error-color;
+ }
+}
+
+.form-textarea {
+ resize: vertical;
+ min-height: 60px;
+}
+
+.code-textarea {
+ width: 100%;
+ padding: 16px;
+ background: rgba(0, 0, 0, 0.4);
+ border: 1px solid $card-border;
+ border-radius: 8px;
+ color: $text-light;
+ font-family: "Courier New", monospace;
+ font-size: 0.9rem;
+ line-height: 1.4;
+ resize: vertical;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.4);
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $primary-color;
+ box-shadow: 0 0 0 3px rgba(223, 136, 29, 0.1);
+ }
+}
+
+.error-message {
+ color: $error-color;
+ font-size: 0.85rem;
+ margin-top: 5px;
+ display: block;
+}
+
+// Botones
+.add-button,
+.add-sub-button,
+.add-example-button {
+ background: linear-gradient(45deg, $primary-color, $secondary-color);
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(223, 136, 29, 0.3);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
+.add-sub-button,
+.add-example-button {
+ font-size: 0.85rem;
+ padding: 8px 16px;
+}
+
+.remove-button,
+.remove-sub-button,
+.remove-example-button {
+ background: rgba(255, 82, 82, 0.1);
+ color: $error-color;
+ border: 1px solid rgba(244, 67, 54, 0.5);
+ padding: 8px 14px;
+ border-radius: 6px;
+ font-weight: 500;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &:hover {
+ background: rgba(244, 67, 54, 0.2);
+ border-color: rgba(244, 67, 54, 0.7);
+ transform: scale(1.05);
+ }
+
+ &:focus {
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(244, 67, 54, 0.2);
+ }
+}
+
+// Contenedores de contenido con jerarquía visual mejorada
+.contents-container {
+ display: flex;
+ flex-direction: column;
+ gap: 25px;
+}
+
+.content-card {
+ background: rgba(255, 255, 255, 0.08);
+ border: 2px solid rgba(138, 99, 210, 0.3);
+ border-left: 4px solid $secondary-color;
+ border-radius: 10px;
+ margin-bottom: 2rem;
+ position: relative;
+ backdrop-filter: blur(10px);
+}
+
+.content-header {
+ background: rgba(138, 99, 210, 0.15);
+ padding: 1rem;
+ border-bottom: 1px solid rgba(138, 99, 210, 0.2);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0;
+
+ h3 {
+ color: $text-light;
+ font-size: 1.2rem;
+ font-weight: 600;
+ margin: 0;
+ }
+}
+
+.content-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.content-number {
+ background: $secondary-color;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 50%;
+ font-weight: bold;
+ font-size: 0.875rem;
+ min-width: 2rem;
+ text-align: center;
+}
+
+.content-body {
+ padding: 1.5rem;
+}
+
+.subcontent-section {
+ margin-top: 2rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ padding-top: 1.5rem;
+
+ .subcontent-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+
+ h4 {
+ color: $primary-color;
+ font-size: 1.1rem;
+ font-weight: 600;
+ margin: 0;
+ }
+ }
+}
+
+.subcontent-container {
+ margin-left: 1rem;
+ border-left: 2px solid rgba(255, 255, 255, 0.1);
+ padding-left: 1rem;
+}
+
+.subcontent-card {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ margin-bottom: 1rem;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.subcontent-header-item {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.subcontent-title {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.subcontent-number {
+ background: $primary-color;
+ color: white;
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ font-weight: bold;
+ font-size: 0.75rem;
+}
+
+.subcontent-body {
+ padding: 1rem;
+}
+
+.examples-section {
+ margin-top: 1rem;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+ padding-top: 1rem;
+
+ .examples-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ h5 {
+ color: rgba(76, 175, 80, 0.9);
+ margin: 0;
+ font-size: 1rem;
+ }
+ }
+}
+
+.examples-container {
+ margin-left: 1rem;
+ border-left: 2px solid rgba(255, 255, 255, 0.05);
+ padding-left: 1rem;
+}
+
+.example-item {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 6px;
+ margin-bottom: 0.75rem;
+ overflow: hidden;
+ position: relative;
+}
+
+.example-header {
+ background: rgba(76, 175, 80, 0.15);
+ padding: 0.5rem 0.75rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ border-bottom: 1px solid rgba(76, 175, 80, 0.2);
+}
+
+.example-number {
+ background: $success-color;
+ color: white;
+ padding: 0.15rem 0.3rem;
+ border-radius: 3px;
+ font-weight: bold;
+ font-size: 0.7rem;
+}
+
+.example-label {
+ font-size: 0.875rem;
+ color: rgba(76, 175, 80, 0.9);
+ font-weight: 500;
+}
+
+// Sobrescribimos el code-textarea dentro de examples para mantener coherencia
+.example-item .code-textarea {
+ width: 100%;
+ border: none;
+ background: rgba(0, 0, 0, 0.6);
+ font-family: "Courier New", monospace;
+ padding: 0.75rem;
+ resize: vertical;
+ min-height: 120px;
+ color: $text-light;
+ border-radius: 0;
+
+ &::placeholder {
+ color: rgba(255, 255, 255, 0.4);
+ }
+
+ &:focus {
+ outline: none;
+ border-color: $primary-color;
+ box-shadow: 0 0 0 3px rgba(223, 136, 29, 0.1);
+ }
+}
+
+.remove-example-button {
+ margin-left: auto;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+}
+
+// Acciones del formulario
+.form-actions {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 40px;
+ padding: 0 120px;
+
+ @media (max-width: 768px) {
+ padding: 0 20px;
+ flex-direction: column;
+ }
+}
+
+.cancel-button {
+ background: transparent;
+ color: $text-muted;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: $text-light;
+ }
+}
+
+.submit-button {
+ background: linear-gradient(45deg, $primary-color, $secondary-color);
+ color: white;
+ border: none;
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 150px;
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(223, 136, 29, 0.4);
+ }
+
+ &:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+ }
+
+ .fa-spinner {
+ margin-right: 8px;
+ }
+}
+
+// Vista previa
+.preview-section {
+ max-width: 1200px;
+ margin: 40px auto 0;
+ padding: 0 120px;
+
+ @media (max-width: 768px) {
+ padding: 0 20px;
+ }
+
+ h3 {
+ color: $text-light;
+ margin-bottom: 15px;
+ font-size: 1.2rem;
+ }
+
+ .json-preview {
+ background: rgba(0, 0, 0, 0.6);
+ border: 1px solid $card-border;
+ border-radius: 8px;
+ padding: 20px;
+ color: $text-light;
+ font-family: "Courier New", monospace;
+ font-size: 0.85rem;
+ line-height: 1.4;
+ max-height: 400px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ }
+}
+
+// Responsive
+@media (max-width: 576px) {
+ .form-actions {
+ flex-direction: column;
+
+ .cancel-button,
+ .submit-button {
+ width: 100%;
+ }
+ }
+
+ .section-header {
+ .add-button {
+ width: 100%;
+ justify-content: center;
+ }
+ }
+}
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.spec.ts b/frontend/angular/src/app/components/form-course/form-course.component.spec.ts
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.ts b/frontend/angular/src/app/components/form-course/form-course.component.ts
new file mode 100644
index 0000000..6ca0134
--- /dev/null
+++ b/frontend/angular/src/app/components/form-course/form-course.component.ts
@@ -0,0 +1,336 @@
+import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
+import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
+import { CommonModule } from '@angular/common';
+import { ReactiveFormsModule } from '@angular/forms';
+
+interface CourseData {
+ course: {
+ title: string;
+ description: string;
+ goto: string;
+ };
+ contents: ContentData[];
+}
+
+interface ContentData {
+ title: string;
+ paragraph: string[];
+ subcontent: SubcontentData[];
+ next: string | null;
+}
+
+interface SubcontentData {
+ subtitle: string;
+ subparagraph: string[];
+ example: string[];
+}
+
+interface FormContentValue {
+ title: string;
+ paragraph: string;
+ subcontent: FormSubcontentValue[];
+}
+
+interface FormSubcontentValue {
+ subtitle: string;
+ subparagraph: string;
+ example: string[];
+}
+
+interface FormValue {
+ title: string;
+ description: string;
+ contents: FormContentValue[];
+}
+
+@Component({
+ selector: 'app-form-course',
+ standalone: true,
+ templateUrl: './form-course.component.html',
+ styleUrls: ['./form-course.component.scss'],
+ imports: [CommonModule, ReactiveFormsModule]
+})
+export class FormCourseComponent implements OnInit, OnChanges {
+ @Input() initialData?: CourseData; // Para modo edición
+ @Input() showPreview = false;
+ @Output() formDataChange = new EventEmitter();
+ @Output() formValidChange = new EventEmitter();
+
+ courseForm: FormGroup;
+
+ constructor(private fb: FormBuilder) {
+ this.courseForm = this.createCourseForm();
+ }
+
+ ngOnInit(): void {
+ // Agregar el primer contenido por defecto si no hay datos iniciales
+ if (this.contents.length === 0 && !this.initialData) {
+ this.addContent();
+ }
+
+ // Suscribirse a cambios del formulario
+ this.courseForm.valueChanges.subscribe(() => {
+ this.emitFormData();
+ });
+
+ this.courseForm.statusChanges.subscribe(() => {
+ this.emitFormValid();
+ });
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['initialData'] && changes['initialData'].currentValue) {
+ this.loadInitialData(changes['initialData'].currentValue);
+ }
+ }
+
+ private createCourseForm(): FormGroup {
+ return this.fb.group({
+ title: ['', [Validators.required, Validators.minLength(3)]],
+ description: ['', [Validators.required, Validators.minLength(10)]],
+ contents: this.fb.array([])
+ });
+ }
+
+ private loadInitialData(data: CourseData): void {
+ // Limpiar contenidos existentes
+ this.contents.clear();
+
+ // Cargar datos del curso
+ this.courseForm.patchValue({
+ title: data.course.title,
+ description: data.course.description
+ });
+
+ // Cargar contenidos
+ data.contents.forEach(content => {
+ const contentGroup = this.fb.group({
+ title: [content.title, Validators.required],
+ paragraph: [content.paragraph.join('\n'), Validators.required],
+ subcontent: this.fb.array([])
+ });
+
+ // Cargar subcontenidos
+ content.subcontent.forEach(subcontent => {
+ const subcontentGroup = this.fb.group({
+ subtitle: [subcontent.subtitle, Validators.required],
+ subparagraph: [subcontent.subparagraph.join('\n'), Validators.required],
+ example: this.fb.array(
+ subcontent.example.map(example => this.fb.control(example, Validators.required))
+ )
+ });
+
+ (contentGroup.get('subcontent') as FormArray).push(subcontentGroup);
+ });
+
+ this.contents.push(contentGroup);
+ });
+
+ this.emitFormData();
+ this.emitFormValid();
+ }
+
+ // Getters para FormArrays
+ get contents(): FormArray {
+ return this.courseForm.get('contents') as FormArray;
+ }
+
+ getSubcontents(contentIndex: number): FormArray {
+ return this.contents.at(contentIndex).get('subcontent') as FormArray;
+ }
+
+ getExamples(contentIndex: number, subcontentIndex: number): FormArray {
+ return this.getSubcontents(contentIndex).at(subcontentIndex).get('example') as FormArray;
+ }
+
+ // Métodos para manejar contenidos
+ addContent(): void {
+ if (this.contents.length < 20) {
+ const contentGroup = this.fb.group({
+ title: ['', Validators.required],
+ paragraph: ['', Validators.required],
+ subcontent: this.fb.array([])
+ });
+
+ this.contents.push(contentGroup);
+
+ // Agregar el primer subcontenido automáticamente
+ this.addSubcontent(this.contents.length - 1);
+
+ // Actualizar las referencias "next" automáticamente
+ this.updateNextOptions();
+ }
+ }
+
+ removeContent(index: number): void {
+ if (this.contents.length > 1) {
+ this.contents.removeAt(index);
+ this.updateNextOptions();
+ }
+ }
+
+ // Métodos para manejar subcontenidos
+ addSubcontent(contentIndex: number): void {
+ const subcontentGroup = this.fb.group({
+ subtitle: ['', Validators.required],
+ subparagraph: ['', Validators.required],
+ example: this.fb.array([this.fb.control('', Validators.required)])
+ });
+
+ this.getSubcontents(contentIndex).push(subcontentGroup);
+ }
+
+ removeSubcontent(contentIndex: number, subcontentIndex: number): void {
+ const subcontents = this.getSubcontents(contentIndex);
+ if (subcontents.length > 1) {
+ subcontents.removeAt(subcontentIndex);
+ }
+ }
+
+ // Métodos para manejar ejemplos
+ addExample(contentIndex: number, subcontentIndex: number): void {
+ this.getExamples(contentIndex, subcontentIndex).push(
+ this.fb.control('', Validators.required)
+ );
+ }
+
+ removeExample(contentIndex: number, subcontentIndex: number, exampleIndex: number): void {
+ const examples = this.getExamples(contentIndex, subcontentIndex);
+ if (examples.length > 1) {
+ examples.removeAt(exampleIndex);
+ }
+ }
+
+ // Actualizar opciones de "siguiente contenido" automáticamente
+ updateNextOptions(): void {
+ // Esta función ahora solo actualiza los valores "next" internamente
+ // No se muestra en el formulario, pero se usa en el JSON final
+ }
+
+ // Generar ruta de acceso automáticamente desde el título
+ private generateGoto(title: string): string {
+ return title
+ .toLowerCase()
+ .replace(/[^a-z0-9\s]/g, '') // Eliminar caracteres especiales
+ .replace(/\s+/g, '-') // Reemplazar espacios con guiones
+ .trim();
+ }
+
+ // Formatear datos para el JSON final
+ private formatCourseData(): CourseData {
+ const formValue = this.courseForm.value as FormValue;
+
+ const contents: ContentData[] = formValue.contents.map((content: FormContentValue, index: number) => {
+ const contentData: ContentData = {
+ title: content.title,
+ paragraph: [content.paragraph],
+ subcontent: content.subcontent.map((sub: FormSubcontentValue) => ({
+ subtitle: sub.subtitle,
+ subparagraph: [sub.subparagraph],
+ example: sub.example.filter((ex: string) => ex.trim() !== '')
+ })),
+ next: null // Inicializar next como null por defecto
+ };
+
+ // Agregar "next" automáticamente si no es el último contenido
+ if (index < formValue.contents.length - 1) {
+ const nextContent = formValue.contents[index + 1];
+ // Verificar que el siguiente contenido existe y tiene título
+ if (nextContent && nextContent.title && nextContent.title.trim() !== '') {
+ contentData.next = nextContent.title.trim();
+ }
+ }
+
+ return contentData;
+ });
+
+ return {
+ course: {
+ title: formValue.title,
+ description: formValue.description,
+ goto: this.generateGoto(formValue.title)
+ },
+ contents
+ };
+ }
+
+ // Vista previa del JSON
+ getPreviewJson(): string {
+ if (this.courseForm.valid) {
+ return JSON.stringify(this.formatCourseData(), null, 2);
+ }
+ return 'Completa todos los campos requeridos para ver la vista previa';
+ }
+
+ // Validación personalizada
+ validateCourse(): boolean {
+ // Validar que cada contenido tenga al menos un subcontenido
+ for (let i = 0; i < this.contents.length; i++) {
+ const subcontents = this.getSubcontents(i);
+ if (subcontents.length === 0) {
+ alert(`El contenido ${i + 1} debe tener al menos un subcontenido`);
+ return false;
+ }
+
+ // Validar que cada subcontenido tenga al menos un ejemplo
+ for (let j = 0; j < subcontents.length; j++) {
+ const examples = this.getExamples(i, j);
+ const validExamples = examples.controls.filter(ex => ex.value.trim() !== '');
+ if (validExamples.length === 0) {
+ alert(`El subcontenido ${j + 1} del contenido ${i + 1} debe tener al menos un ejemplo`);
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ // Marcar todos los campos como tocados para mostrar errores
+ markFormGroupTouched(): void {
+ this.markFormGroupTouchedRecursive(this.courseForm);
+ }
+
+ private markFormGroupTouchedRecursive(formGroup: FormGroup | FormArray): void {
+ Object.keys(formGroup.controls).forEach(key => {
+ const control = formGroup.get(key);
+ if (control instanceof FormGroup || control instanceof FormArray) {
+ this.markFormGroupTouchedRecursive(control);
+ } else {
+ control?.markAsTouched();
+ }
+ });
+ }
+
+ // Método para actualizar ruta automáticamente cuando cambia el título
+ onTitleChange(): void {
+ // Este método se mantiene para compatibilidad con el template
+ // La generación de goto ahora es automática en formatCourseData()
+ }
+
+ // Métodos para emitir cambios al componente padre
+ private emitFormData(): void {
+ if (this.courseForm.valid) {
+ this.formDataChange.emit(this.formatCourseData());
+ }
+ }
+
+ private emitFormValid(): void {
+ this.formValidChange.emit(this.courseForm.valid && this.validateCourse());
+ }
+
+ // Método para obtener los datos del formulario (para uso externo)
+ getFormData(): CourseData | null {
+ if (this.courseForm.valid && this.validateCourse()) {
+ return this.formatCourseData();
+ }
+ return null;
+ }
+
+ // Método para resetear el formulario
+ resetForm(): void {
+ this.courseForm.reset();
+ this.contents.clear();
+ this.addContent();
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.html b/frontend/angular/src/app/pages/create-course/create-course.component.html
index 87fb10a..ab93853 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.html
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.html
@@ -6,208 +6,32 @@ Crear Nuevo Curso
-
-
-
-
-
Vista Previa del JSON
-
{{ getPreviewJson() }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.scss b/frontend/angular/src/app/pages/create-course/create-course.component.scss
index fc61aed..d059200 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.scss
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.scss
@@ -50,410 +50,6 @@ $success-color: #4caf50;
}
}
-.course-form {
- max-width: 1200px;
- margin: 0 auto;
- padding: 0 120px;
-
- @media (max-width: 768px) {
- padding: 0 20px;
- }
-}
-
-.form-section {
- background: $card-bg;
- border: 1px solid $card-border;
- border-radius: 12px;
- padding: 30px;
- margin-bottom: 30px;
- backdrop-filter: blur(10px);
-
- h2 {
- font-size: 1.5rem;
- margin-bottom: 25px;
- font-weight: 600;
- position: relative;
- color: $text-light;
-
- &:after {
- content: "";
- position: absolute;
- bottom: -8px;
- left: 0;
- width: 60px;
- height: 3px;
- background: linear-gradient(90deg, $primary-color, $secondary-color);
- border-radius: 2px;
- }
- }
-}
-
-.section-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 25px;
-
- @media (max-width: 576px) {
- flex-direction: column;
- gap: 15px;
- align-items: stretch;
- }
-}
-
-.form-group {
- margin-bottom: 20px;
-
- label {
- display: block;
- margin-bottom: 8px;
- font-weight: 500;
- color: $text-light;
- font-size: 0.95rem;
- }
-}
-
-.form-input,
-.form-textarea,
-.form-select {
- width: 100%;
- padding: 12px 16px;
- background: rgba(255, 255, 255, 0.1);
- border: 1px solid $card-border;
- border-radius: 8px;
- color: $text-light;
- font-size: 0.95rem;
- transition: all 0.3s ease;
-
- &::placeholder {
- color: rgba(255, 255, 255, 0.5);
- }
-
- &:focus {
- outline: none;
- border-color: $secondary-color;
- box-shadow: 0 0 0 3px rgba(138, 99, 210, 0.1);
- background: rgba(255, 255, 255, 0.15);
- }
-
- &:invalid {
- border-color: $error-color;
- }
-}
-
-.form-textarea {
- resize: vertical;
- min-height: 60px;
-}
-
-.code-textarea {
- width: 100%;
- padding: 16px;
- background: rgba(0, 0, 0, 0.4);
- border: 1px solid $card-border;
- border-radius: 8px;
- color: $text-light;
- font-family: "Courier New", monospace;
- font-size: 0.9rem;
- line-height: 1.4;
- resize: vertical;
-
- &::placeholder {
- color: rgba(255, 255, 255, 0.4);
- }
-
- &:focus {
- outline: none;
- border-color: $primary-color;
- box-shadow: 0 0 0 3px rgba(223, 136, 29, 0.1);
- }
-}
-
-.error-message {
- color: $error-color;
- font-size: 0.85rem;
- margin-top: 5px;
- display: block;
-}
-
-// Botones
-.add-button,
-.add-sub-button,
-.add-example-button {
- background: linear-gradient(45deg, $primary-color, $secondary-color);
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 6px;
- font-weight: 600;
- cursor: pointer;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- gap: 8px;
-
- &:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(223, 136, 29, 0.3);
- }
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- i {
- font-size: 0.9rem;
- }
-}
-
-.add-sub-button,
-.add-example-button {
- font-size: 0.85rem;
- padding: 8px 16px;
-}
-
-.remove-button,
-.remove-sub-button,
-.remove-example-button {
- background: rgba(255, 82, 82, 0.1); // rojo translúcido sobre fondo oscuro
- color: $error-color;
- border: 1px solid rgba(244, 67, 54, 0.5);
- padding: 8px 14px;
- border-radius: 6px;
- font-weight: 500;
- cursor: pointer;
- font-size: 0.9rem;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- gap: 8px;
-
- i {
- font-size: 0.9rem;
- }
-
- &:hover {
- background: rgba(244, 67, 54, 0.2);
- border-color: rgba(244, 67, 54, 0.7);
- transform: scale(1.05);
- }
-
- &:focus {
- outline: none;
- box-shadow: 0 0 0 3px rgba(244, 67, 54, 0.2);
- }
-}
-// Contenedores de contenido con jerarquía visual mejorada
-.contents-container {
- display: flex;
- flex-direction: column;
- gap: 25px;
-}
-
-.content-card {
- background: rgba(255, 255, 255, 0.08);
- border: 2px solid rgba(138, 99, 210, 0.3);
- border-left: 4px solid $secondary-color;
- border-radius: 10px;
- margin-bottom: 2rem;
- position: relative;
- backdrop-filter: blur(10px);
-}
-
-.content-header {
- background: rgba(138, 99, 210, 0.15);
- padding: 1rem;
- border-bottom: 1px solid rgba(138, 99, 210, 0.2);
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 0;
-
- h3 {
- color: $text-light;
- font-size: 1.2rem;
- font-weight: 600;
- margin: 0;
- }
-}
-
-.content-title {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.content-number {
- background: $secondary-color;
- color: white;
- padding: 0.25rem 0.5rem;
- border-radius: 50%;
- font-weight: bold;
- font-size: 0.875rem;
- min-width: 2rem;
- text-align: center;
-}
-
-.content-body {
- padding: 1.5rem;
-}
-
-.subcontent-section {
- margin-top: 2rem;
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- padding-top: 1.5rem;
-
- .subcontent-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
-
- h4 {
- color: $primary-color;
- font-size: 1.1rem;
- font-weight: 600;
- margin: 0;
- }
- }
-}
-
-.subcontent-container {
- margin-left: 1rem;
- border-left: 2px solid rgba(255, 255, 255, 0.1);
- padding-left: 1rem;
-}
-
-.subcontent-card {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 8px;
- margin-bottom: 1rem;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
-}
-
-.subcontent-header-item {
- background: rgba(255, 255, 255, 0.08);
- padding: 0.75rem 1rem;
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.subcontent-title {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.subcontent-number {
- background: $primary-color;
- color: white;
- padding: 0.2rem 0.4rem;
- border-radius: 4px;
- font-weight: bold;
- font-size: 0.75rem;
-}
-
-.subcontent-body {
- padding: 1rem;
-}
-
-.examples-section {
- margin-top: 1rem;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- padding-top: 1rem;
-
- .examples-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
-
- label {
- margin-bottom: 0;
- font-size: 0.9rem;
- color: $text-muted;
- }
-
- h5 {
- color: rgba(76, 175, 80, 0.9);
- margin: 0;
- font-size: 1rem;
- }
- }
-}
-
-.examples-container {
- margin-left: 1rem;
- border-left: 2px solid rgba(255, 255, 255, 0.05);
- padding-left: 1rem;
-}
-
-.example-item {
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 6px;
- margin-bottom: 0.75rem;
- overflow: hidden;
- position: relative;
-}
-
-.example-header {
- background: rgba(76, 175, 80, 0.15);
- padding: 0.5rem 0.75rem;
- display: flex;
- align-items: center;
- gap: 0.5rem;
- border-bottom: 1px solid rgba(76, 175, 80, 0.2);
-}
-
-.example-number {
- background: $success-color;
- color: white;
- padding: 0.15rem 0.3rem;
- border-radius: 3px;
- font-weight: bold;
- font-size: 0.7rem;
-}
-
-.example-label {
- font-size: 0.875rem;
- color: rgba(76, 175, 80, 0.9);
- font-weight: 500;
-}
-
-// Sobrescribimos el code-textarea dentro de examples para mantener coherencia
-.example-item .code-textarea {
- width: 100%;
- border: none;
- background: rgba(0, 0, 0, 0.6);
- font-family: "Courier New", monospace;
- padding: 0.75rem;
- resize: vertical;
- min-height: 120px;
- color: $text-light;
- border-radius: 0;
-
- &::placeholder {
- color: rgba(255, 255, 255, 0.4);
- }
-
- &:focus {
- outline: none;
- border-color: $primary-color;
- box-shadow: 0 0 0 3px rgba(223, 136, 29, 0.1);
- }
-}
-
-.remove-example-button {
- margin-left: auto;
- padding: 0.25rem 0.5rem;
- font-size: 0.75rem;
-}
-
// Acciones del formulario
.form-actions {
display: flex;
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.ts b/frontend/angular/src/app/pages/create-course/create-course.component.ts
index a1c96eb..ca33328 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.ts
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.ts
@@ -1,10 +1,9 @@
-import { Component, OnInit } from '@angular/core';
-import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms';
+import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
-import { ReactiveFormsModule } from '@angular/forms';
-import { CoursesService } from '../../services/courses.service'; // Ajusta la ruta según tu estructura
+import { FormCourseComponent } from '../../components/form-course/form-course.component';
+// Interfaces para el tipado de datos
interface CourseData {
course: {
title: string;
@@ -27,281 +26,100 @@ interface SubcontentData {
example: string[];
}
-interface FormContentValue {
- title: string;
- paragraph: string;
- subcontent: FormSubcontentValue[];
-}
-
-interface FormSubcontentValue {
- subtitle: string;
- subparagraph: string;
- example: string[];
-}
-
-interface FormValue {
- title: string;
- description: string;
- contents: FormContentValue[];
-}
-
@Component({
selector: 'app-create-course',
standalone: true,
templateUrl: './create-course.component.html',
styleUrls: ['./create-course.component.scss'],
- imports: [CommonModule, ReactiveFormsModule]
+ imports: [CommonModule, FormCourseComponent]
})
-export class CreateCourseComponent implements OnInit {
- courseForm: FormGroup;
+export class CreateCourseComponent {
+ // Estados del componente
isSubmitting = false;
+ isFormValid = false;
showPreview = false;
+ currentCourseData: CourseData | null = null;
- constructor(
- private fb: FormBuilder,
- private router: Router,
- private coursesService: CoursesService
- ) {
- this.courseForm = this.createCourseForm();
- }
-
- ngOnInit(): void {
- // Agregar el primer contenido por defecto
- if (this.contents.length === 0) {
- this.addContent();
- }
- }
-
- private createCourseForm(): FormGroup {
- return this.fb.group({
- title: ['', [Validators.required, Validators.minLength(3)]],
- description: ['', [Validators.required, Validators.minLength(10)]],
- contents: this.fb.array([])
- });
- }
-
- // Getters para FormArrays
- get contents(): FormArray {
- return this.courseForm.get('contents') as FormArray;
- }
-
- getSubcontents(contentIndex: number): FormArray {
- return this.contents.at(contentIndex).get('subcontent') as FormArray;
- }
-
- getExamples(contentIndex: number, subcontentIndex: number): FormArray {
- return this.getSubcontents(contentIndex).at(subcontentIndex).get('example') as FormArray;
- }
-
- // Métodos para manejar contenidos
- addContent(): void {
- if (this.contents.length < 20) {
- const contentGroup = this.fb.group({
- title: ['', Validators.required],
- paragraph: ['', Validators.required],
- subcontent: this.fb.array([])
- });
-
- this.contents.push(contentGroup);
-
- // Agregar el primer subcontenido automáticamente
- this.addSubcontent(this.contents.length - 1);
-
- // Actualizar las referencias "next" automáticamente
- this.updateNextOptions();
- }
- }
+ constructor(private router: Router) {}
- removeContent(index: number): void {
- if (this.contents.length > 1) {
- this.contents.removeAt(index);
- this.updateNextOptions();
- }
+ // Manejar cambios en los datos del formulario
+ onFormDataChange(courseData: CourseData): void {
+ this.currentCourseData = courseData;
+ console.log('Datos del curso actualizados:', courseData);
}
- // Métodos para manejar subcontenidos
- addSubcontent(contentIndex: number): void {
- const subcontentGroup = this.fb.group({
- subtitle: ['', Validators.required],
- subparagraph: ['', Validators.required],
- example: this.fb.array([this.fb.control('', Validators.required)])
- });
-
- this.getSubcontents(contentIndex).push(subcontentGroup);
- }
-
- removeSubcontent(contentIndex: number, subcontentIndex: number): void {
- const subcontents = this.getSubcontents(contentIndex);
- if (subcontents.length > 1) {
- subcontents.removeAt(subcontentIndex);
- }
- }
-
- // Métodos para manejar ejemplos
- addExample(contentIndex: number, subcontentIndex: number): void {
- this.getExamples(contentIndex, subcontentIndex).push(
- this.fb.control('', Validators.required)
- );
- }
-
- removeExample(contentIndex: number, subcontentIndex: number, exampleIndex: number): void {
- const examples = this.getExamples(contentIndex, subcontentIndex);
- if (examples.length > 1) {
- examples.removeAt(exampleIndex);
- }
- }
-
- // Actualizar opciones de "siguiente contenido" automáticamente
- updateNextOptions(): void {
- // Esta función ahora solo actualiza los valores "next" internamente
- // No se muestra en el formulario, pero se usa en el JSON final
- }
-
- // Generar ruta de acceso automáticamente desde el título
- private generateGoto(title: string): string {
- return title
- .toLowerCase()
- .replace(/[^a-z0-9\s]/g, '') // Eliminar caracteres especiales
- .replace(/\s+/g, '-') // Reemplazar espacios con guiones
- .trim();
- }
-
- // Formatear datos para el JSON final
- private formatCourseData(): CourseData {
- const formValue = this.courseForm.value as FormValue;
-
- const contents: ContentData[] = formValue.contents.map((content: FormContentValue, index: number) => {
- const contentData: ContentData = {
- title: content.title,
- paragraph: [content.paragraph],
- subcontent: content.subcontent.map((sub: FormSubcontentValue) => ({
- subtitle: sub.subtitle,
- subparagraph: [sub.subparagraph],
- example: sub.example.filter((ex: string) => ex.trim() !== '')
- })),
- next: null // Inicializar next como null por defecto
- };
-
- // Agregar "next" automáticamente si no es el último contenido
- if (index < formValue.contents.length - 1) {
- const nextContent = formValue.contents[index + 1];
- // Verificar que el siguiente contenido existe y tiene título
- if (nextContent && nextContent.title && nextContent.title.trim() !== '') {
- contentData.next = nextContent.title.trim();
- }
- }
-
- return contentData;
- });
-
- return {
- course: {
- title: formValue.title,
- description: formValue.description,
- goto: this.generateGoto(formValue.title)
- },
- contents
- };
- }
-
- // Vista previa del JSON
- getPreviewJson(): string {
- if (this.courseForm.valid) {
- return JSON.stringify(this.formatCourseData(), null, 2);
- }
- return 'Completa todos los campos requeridos para ver la vista previa';
+ // Manejar cambios en la validez del formulario
+ onFormValidChange(isValid: boolean): void {
+ this.isFormValid = isValid;
+ console.log('Estado de validez del formulario:', isValid);
}
+ // Alternar vista previa del JSON
togglePreview(): void {
this.showPreview = !this.showPreview;
}
- // Validación personalizada
- private validateCourse(): boolean {
- // Validar que cada contenido tenga al menos un subcontenido
- for (let i = 0; i < this.contents.length; i++) {
- const subcontents = this.getSubcontents(i);
- if (subcontents.length === 0) {
- alert(`El contenido ${i + 1} debe tener al menos un subcontenido`);
- return false;
- }
-
- // Validar que cada subcontenido tenga al menos un ejemplo
- for (let j = 0; j < subcontents.length; j++) {
- const examples = this.getExamples(i, j);
- const validExamples = examples.controls.filter(ex => ex.value.trim() !== '');
- if (validExamples.length === 0) {
- alert(`El subcontenido ${j + 1} del contenido ${i + 1} debe tener al menos un ejemplo`);
- return false;
- }
- }
+ // Enviar formulario
+ async onSubmit(): Promise {
+ if (!this.isFormValid || !this.currentCourseData) {
+ console.error('Formulario inválido o sin datos');
+ return;
}
- return true;
- }
+ this.isSubmitting = true;
- // MÉTODO ACTUALIZADO: Envío del formulario con integración a la API
- onSubmit(): void {
- if (this.courseForm.valid && this.validateCourse()) {
- this.isSubmitting = true;
+ try {
+ // Aquí puedes implementar tu lógica de envío
+ // Por ejemplo, llamar a un servicio para guardar el curso
+ console.log('Enviando curso:', this.currentCourseData);
- const courseData = this.formatCourseData();
+ // Simular una llamada asíncrona
+ await this.saveCourse(this.currentCourseData);
- // Enviar el curso a la API
- this.coursesService.createCourse(courseData).subscribe({
- next: (response) => {
- console.log('Curso creado exitosamente:', response);
-
- // Mostrar mensaje de éxito
- alert(`¡Curso creado exitosamente! ID: ${response.id}`);
-
- // Opcional: Resetear el formulario o navegar a otra página
- this.courseForm.reset();
- this.contents.clear();
- this.addContent(); // Agregar contenido inicial
-
- // Opcional: Navegar a la lista de cursos o a ver el curso creado
- this.router.navigate(['/cursos']);
-
- this.isSubmitting = false;
- },
- error: (error) => {
- console.error('Error al crear el curso:', error);
- alert('Error al crear el curso: ' + error.message);
- this.isSubmitting = false;
- }
- });
+ // Mostrar mensaje de éxito y redirigir
+ alert('¡Curso creado exitosamente!');
+ this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
- } else {
- this.markFormGroupTouched(this.courseForm);
- alert('Por favor, completa todos los campos requeridos');
+ } catch (error) {
+ console.error('Error al crear el curso:', error);
+ alert('Error al crear el curso. Por favor, inténtalo de nuevo.');
+ } finally {
+ this.isSubmitting = false;
}
}
- onCancel(): void {
- if (confirm('¿Estás seguro de que quieres cancelar? Se perderán todos los cambios.')) {
- this.courseForm.reset();
- this.contents.clear();
- this.addContent(); // Agregar contenido inicial
- this.router.navigate(['/cursos'])
- }
+ // Simular guardado del curso (reemplaza con tu lógica real)
+ private async saveCourse(courseData: CourseData): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Simular éxito o error
+ const success = Math.random() > 0.1; // 90% de probabilidad de éxito
+
+ if (success) {
+ resolve();
+ } else {
+ reject(new Error('Error simulado en el servidor'));
+ }
+ }, 2000); // Simular 2 segundos de carga
+ });
}
- // Marcar todos los campos como tocados para mostrar errores
- private markFormGroupTouched(formGroup: FormGroup | FormArray): void {
- Object.keys(formGroup.controls).forEach(key => {
- const control = formGroup.get(key);
- if (control instanceof FormGroup || control instanceof FormArray) {
- this.markFormGroupTouched(control);
- } else {
- control?.markAsTouched();
+ // Cancelar creación
+ onCancel(): void {
+ const hasChanges = this.currentCourseData !== null;
+
+ if (hasChanges) {
+ const confirmLeave = confirm('¿Estás seguro de que quieres cancelar? Se perderán todos los cambios.');
+ if (!confirmLeave) {
+ return;
}
- });
+ }
+
+ this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
}
- // Método para actualizar ruta automáticamente cuando cambia el título
- onTitleChange(): void {
- // Este método se mantiene para compatibilidad con el template
- // La generación de goto ahora es automática en formatCourseData()
+ // Método para obtener datos del curso (útil para debugging)
+ getCourseData(): CourseData | null {
+ return this.currentCourseData;
}
}
\ No newline at end of file
From 74adeb96c755c9d10d77aaef8db6c52166b23373 Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 19:31:51 -0400
Subject: [PATCH 04/15] campos de tiempo maximo y recursos maximos incluidos en
el formulario de curso
---
.../form-course/form-course.component.html | 71 ++++++++++--
.../form-course/form-course.component.ts | 108 +++++++++++++++---
2 files changed, 152 insertions(+), 27 deletions(-)
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.html b/frontend/angular/src/app/components/form-course/form-course.component.html
index 55d521d..9a0f3fd 100644
--- a/frontend/angular/src/app/components/form-course/form-course.component.html
+++ b/frontend/angular/src/app/components/form-course/form-course.component.html
@@ -39,7 +39,7 @@ 📖 Contenidos del Curso
class="add-button"
(click)="addContent()"
[disabled]="contents.length >= 20">
- ➕ Agregar Contenido
+ ➕ Agregar Contenido ({{ contents.length }}/20)
@@ -92,8 +92,9 @@ 📋 Subcontenidos
@@ -145,18 +146,21 @@ 💻 Ejemplos de Código
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.ts b/frontend/angular/src/app/components/form-course/form-course.component.ts
index 6ca0134..7d495bb 100644
--- a/frontend/angular/src/app/components/form-course/form-course.component.ts
+++ b/frontend/angular/src/app/components/form-course/form-course.component.ts
@@ -22,7 +22,13 @@ interface ContentData {
interface SubcontentData {
subtitle: string;
subparagraph: string[];
- example: string[];
+ example: ExampleData[];
+}
+
+interface ExampleData {
+ code: string;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface FormContentValue {
@@ -34,7 +40,13 @@ interface FormContentValue {
interface FormSubcontentValue {
subtitle: string;
subparagraph: string;
- example: string[];
+ example: FormExampleValue[];
+}
+
+interface FormExampleValue {
+ code: string;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface FormValue {
@@ -43,6 +55,21 @@ interface FormValue {
contents: FormContentValue[];
}
+// Custom validators
+function positiveIntegerValidator(control: any) {
+ const value = control.value;
+ if (value === null || value === undefined || value === '') {
+ return null; // Let required validator handle empty values
+ }
+
+ const numValue = Number(value);
+ if (!Number.isInteger(numValue) || numValue <= 0) {
+ return { positiveInteger: true };
+ }
+
+ return null;
+}
+
@Component({
selector: 'app-form-course',
standalone: true,
@@ -92,6 +119,20 @@ export class FormCourseComponent implements OnInit, OnChanges {
});
}
+ private createExampleFormGroup(example?: ExampleData): FormGroup {
+ return this.fb.group({
+ code: [example?.code || '', Validators.required],
+ maxResourceConsumption: [
+ example?.maxResourceConsumption || '',
+ [Validators.required, positiveIntegerValidator]
+ ],
+ maxProcessingTime: [
+ example?.maxProcessingTime || '',
+ [Validators.required, positiveIntegerValidator]
+ ]
+ });
+ }
+
private loadInitialData(data: CourseData): void {
// Limpiar contenidos existentes
this.contents.clear();
@@ -116,7 +157,7 @@ export class FormCourseComponent implements OnInit, OnChanges {
subtitle: [subcontent.subtitle, Validators.required],
subparagraph: [subcontent.subparagraph.join('\n'), Validators.required],
example: this.fb.array(
- subcontent.example.map(example => this.fb.control(example, Validators.required))
+ subcontent.example.map(example => this.createExampleFormGroup(example))
)
});
@@ -171,13 +212,16 @@ export class FormCourseComponent implements OnInit, OnChanges {
// Métodos para manejar subcontenidos
addSubcontent(contentIndex: number): void {
- const subcontentGroup = this.fb.group({
- subtitle: ['', Validators.required],
- subparagraph: ['', Validators.required],
- example: this.fb.array([this.fb.control('', Validators.required)])
- });
+ const subcontents = this.getSubcontents(contentIndex);
+ if (subcontents.length < 10) {
+ const subcontentGroup = this.fb.group({
+ subtitle: ['', Validators.required],
+ subparagraph: ['', Validators.required],
+ example: this.fb.array([this.createExampleFormGroup()])
+ });
- this.getSubcontents(contentIndex).push(subcontentGroup);
+ subcontents.push(subcontentGroup);
+ }
}
removeSubcontent(contentIndex: number, subcontentIndex: number): void {
@@ -189,9 +233,10 @@ export class FormCourseComponent implements OnInit, OnChanges {
// Métodos para manejar ejemplos
addExample(contentIndex: number, subcontentIndex: number): void {
- this.getExamples(contentIndex, subcontentIndex).push(
- this.fb.control('', Validators.required)
- );
+ const examples = this.getExamples(contentIndex, subcontentIndex);
+ if (examples.length < 5) {
+ examples.push(this.createExampleFormGroup());
+ }
}
removeExample(contentIndex: number, subcontentIndex: number, exampleIndex: number): void {
@@ -227,7 +272,13 @@ export class FormCourseComponent implements OnInit, OnChanges {
subcontent: content.subcontent.map((sub: FormSubcontentValue) => ({
subtitle: sub.subtitle,
subparagraph: [sub.subparagraph],
- example: sub.example.filter((ex: string) => ex.trim() !== '')
+ example: sub.example
+ .filter((ex: FormExampleValue) => ex.code.trim() !== '')
+ .map((ex: FormExampleValue) => ({
+ code: ex.code,
+ maxResourceConsumption: Number(ex.maxResourceConsumption),
+ maxProcessingTime: Number(ex.maxProcessingTime)
+ }))
})),
next: null // Inicializar next como null por defecto
};
@@ -275,9 +326,13 @@ export class FormCourseComponent implements OnInit, OnChanges {
// Validar que cada subcontenido tenga al menos un ejemplo
for (let j = 0; j < subcontents.length; j++) {
const examples = this.getExamples(i, j);
- const validExamples = examples.controls.filter(ex => ex.value.trim() !== '');
+ const validExamples = examples.controls.filter(ex =>
+ ex.get('code')?.value.trim() !== '' &&
+ ex.get('maxResourceConsumption')?.valid &&
+ ex.get('maxProcessingTime')?.valid
+ );
if (validExamples.length === 0) {
- alert(`El subcontenido ${j + 1} del contenido ${i + 1} debe tener al menos un ejemplo`);
+ alert(`El subcontenido ${j + 1} del contenido ${i + 1} debe tener al menos un ejemplo válido`);
return false;
}
}
@@ -333,4 +388,27 @@ export class FormCourseComponent implements OnInit, OnChanges {
this.contents.clear();
this.addContent();
}
+
+ // Métodos auxiliares para obtener mensajes de error
+ getResourceConsumptionError(contentIndex: number, subcontentIndex: number, exampleIndex: number): string {
+ const control = this.getExamples(contentIndex, subcontentIndex).at(exampleIndex).get('maxResourceConsumption');
+ if (control?.hasError('required')) {
+ return 'El consumo máximo de recursos es requerido';
+ }
+ if (control?.hasError('positiveInteger')) {
+ return 'Debe ser un número entero positivo mayor a 0';
+ }
+ return '';
+ }
+
+ getProcessingTimeError(contentIndex: number, subcontentIndex: number, exampleIndex: number): string {
+ const control = this.getExamples(contentIndex, subcontentIndex).at(exampleIndex).get('maxProcessingTime');
+ if (control?.hasError('required')) {
+ return 'El tiempo máximo de procesamiento es requerido';
+ }
+ if (control?.hasError('positiveInteger')) {
+ return 'Debe ser un número entero positivo mayor a 0';
+ }
+ return '';
+ }
}
\ No newline at end of file
From 6c2c13082024b3109c9c8f060acbcf4bd775848c Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 19:50:00 -0400
Subject: [PATCH 05/15] crear curso en el navbar con traducciones
---
frontend/angular/public/i18n/en.json | 1 +
frontend/angular/public/i18n/es.json | 1 +
frontend/angular/public/i18n/qu.json | 1 +
frontend/angular/src/app/shared/navbar/navbar.component.html | 2 +-
4 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/angular/public/i18n/en.json b/frontend/angular/public/i18n/en.json
index e43a1ce..96a3a5d 100644
--- a/frontend/angular/public/i18n/en.json
+++ b/frontend/angular/public/i18n/en.json
@@ -8,6 +8,7 @@
"terminal": "Terminal",
"chat": "Chat",
"myLearning": "My Learning",
+ "createCourse": "Create Course",
"language": "Language"
},
"HOME": {
diff --git a/frontend/angular/public/i18n/es.json b/frontend/angular/public/i18n/es.json
index 0826429..cd73ec7 100644
--- a/frontend/angular/public/i18n/es.json
+++ b/frontend/angular/public/i18n/es.json
@@ -8,6 +8,7 @@
"terminal": "Terminal",
"chat": "Chat",
"myLearning": "Mi aprendizaje",
+ "createCourse": "Crear curso",
"language": "Idioma"
},
"HOME": {
diff --git a/frontend/angular/public/i18n/qu.json b/frontend/angular/public/i18n/qu.json
index 3496df3..e8b6592 100644
--- a/frontend/angular/public/i18n/qu.json
+++ b/frontend/angular/public/i18n/qu.json
@@ -8,6 +8,7 @@
"terminal": "Terminal",
"chat": "Rimanakuy",
"myLearning": "Ñuqa yachachiyki",
+ "createCourse": "Cursota Paqarichiy",
"language": "Rimay"
},
"HOME": {
diff --git a/frontend/angular/src/app/shared/navbar/navbar.component.html b/frontend/angular/src/app/shared/navbar/navbar.component.html
index 43db559..baf4a62 100644
--- a/frontend/angular/src/app/shared/navbar/navbar.component.html
+++ b/frontend/angular/src/app/shared/navbar/navbar.component.html
@@ -62,7 +62,7 @@ PyBloom
routerLink="/crear-curso"
routerLinkActive="active"
(click)="closeMenu()"
- >{{ "Crear cursos" | translate }}{{ "navbar.createCourse" | translate }}
From ff2bf78a81c635b3b1aaeced789e85c3dd93ff0a Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 20:28:03 -0400
Subject: [PATCH 06/15] editar curso front, carga el JSON en el form
---
frontend/angular/src/app/app.routes.ts | 2 +
.../edit-course/edit-course.component.html | 38 +++
.../edit-course/edit-course.component.scss | 217 +++++++++++++
.../edit-course/edit-course.component.spec.ts | 0
.../edit-course/edit-course.component.ts | 302 ++++++++++++++++++
5 files changed, 559 insertions(+)
create mode 100644 frontend/angular/src/app/pages/edit-course/edit-course.component.html
create mode 100644 frontend/angular/src/app/pages/edit-course/edit-course.component.scss
create mode 100644 frontend/angular/src/app/pages/edit-course/edit-course.component.spec.ts
create mode 100644 frontend/angular/src/app/pages/edit-course/edit-course.component.ts
diff --git a/frontend/angular/src/app/app.routes.ts b/frontend/angular/src/app/app.routes.ts
index a680a0f..1e7dadc 100644
--- a/frontend/angular/src/app/app.routes.ts
+++ b/frontend/angular/src/app/app.routes.ts
@@ -13,6 +13,7 @@ import { CursosComponent } from './pages/cursos/cursos.component';
import { IntroductionComponent } from './pages/introduction/introduction.component';
import { ChatComponent } from './pages/chat/chat.component';
import { CreateCourseComponent } from './pages/create-course/create-course.component';
+import { EditCourseComponent } from './pages/edit-course/edit-course.component';
export const routes: Routes = [
{ path: '', component: HomeComponent },
@@ -29,6 +30,7 @@ export const routes: Routes = [
{ path: 'lista-cursos', component: CoursesListComponent },
{ path: 'terminal', component: TerminalComponent },
{ path: 'cursos/:id', component: IntroductionComponent },
+ { path: 'editar-curso/:id', component: EditCourseComponent },
{ path: 'chat', component: ChatComponent },
{ path: 'crear-curso', component: CreateCourseComponent },
diff --git a/frontend/angular/src/app/pages/edit-course/edit-course.component.html b/frontend/angular/src/app/pages/edit-course/edit-course.component.html
new file mode 100644
index 0000000..78e5605
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.html
@@ -0,0 +1,38 @@
+
+
+
+
Editar Curso
+
Modifica y perfecciona tu curso de programación
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/angular/src/app/pages/edit-course/edit-course.component.scss b/frontend/angular/src/app/pages/edit-course/edit-course.component.scss
new file mode 100644
index 0000000..f8b163b
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.scss
@@ -0,0 +1,217 @@
+@use "sass:color";
+
+// Variables de color
+$primary-color: #4346ff; // azul vibrante
+$secondary-color: #ff6a81; // rosa coral
+$background-dark: #000000; // fondo negro total
+$background-light: #272149; // morado oscuro
+$text-light: #ffffff;
+$text-muted: rgba(255, 255, 255, 0.8);
+$card-bg: rgba(255, 255, 255, 0.05);
+$card-border: rgba(255, 255, 255, 0.1);
+$error-color: #f44336;
+$success-color: #4caf50;
+$warning-color: #ff9800;
+
+.edit-course-container {
+ min-height: 100vh;
+ color: $text-light;
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+ padding-bottom: 60px;
+}
+
+.hero-section {
+ padding: 20px;
+
+ @media (max-width: 768px) {
+ padding: 30px 20px 15px;
+ }
+
+ .hero-content {
+ text-align: center;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+
+ h1 {
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
+ font-weight: 700;
+ color: $text-light;
+ background: linear-gradient(45deg, $primary-color, $secondary-color);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ width: fit-content;
+ }
+
+ .welcome-message {
+ font-size: 1.1rem;
+ opacity: 0.9;
+ color: $text-muted;
+ }
+ }
+}
+
+// Acciones del formulario
+.form-actions {
+ display: flex;
+ justify-content: center;
+ gap: 20px;
+ margin-top: 40px;
+ padding: 0 120px;
+
+ @media (max-width: 768px) {
+ padding: 0 20px;
+ flex-direction: column;
+ }
+}
+
+.cancel-button {
+ background: transparent;
+ color: $text-muted;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: $text-light;
+ border-color: rgba(255, 255, 255, 0.5);
+ }
+}
+
+.preview-button {
+ background: rgba(255, 255, 255, 0.1);
+ color: $text-light;
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.5);
+ }
+
+ &.active {
+ background: linear-gradient(45deg, rgba($primary-color, 0.2), rgba($secondary-color, 0.2));
+ border-color: $primary-color;
+ color: $primary-color;
+ }
+}
+
+.submit-button {
+ background: linear-gradient(45deg, $primary-color, $secondary-color);
+ color: white;
+ border: none;
+ padding: 14px 32px;
+ border-radius: 8px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ min-width: 180px;
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba($primary-color, 0.4);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+ background: rgba(255, 255, 255, 0.1);
+ color: $text-muted;
+ }
+}
+
+// Loading state
+.loading-state {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+ color: $text-muted;
+ font-size: 1.1rem;
+
+ .spinner {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: $primary-color;
+ animation: spin 1s ease-in-out infinite;
+ margin-right: 10px;
+ }
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+// Error state
+.error-state {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ min-height: 200px;
+ color: $error-color;
+ text-align: center;
+ padding: 20px;
+
+ .error-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ }
+
+ .error-title {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ }
+
+ .error-message {
+ color: $text-muted;
+ margin-bottom: 1.5rem;
+ }
+
+ .retry-button {
+ background: $error-color;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 6px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: color.adjust($error-color, $lightness: -10%);
+ }
+ }
+}
+
+// Responsive
+@media (max-width: 576px) {
+ .form-actions {
+ flex-direction: column;
+
+ .cancel-button,
+ .preview-button,
+ .submit-button {
+ width: 100%;
+ }
+ }
+
+ .hero-section .hero-content h1 {
+ font-size: 2rem;
+ }
+}
\ No newline at end of file
diff --git a/frontend/angular/src/app/pages/edit-course/edit-course.component.spec.ts b/frontend/angular/src/app/pages/edit-course/edit-course.component.spec.ts
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/angular/src/app/pages/edit-course/edit-course.component.ts b/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
new file mode 100644
index 0000000..36f69b8
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
@@ -0,0 +1,302 @@
+import { Component, OnInit } from '@angular/core';
+import { Router, ActivatedRoute } from '@angular/router';
+import { CommonModule } from '@angular/common';
+import { FormCourseComponent } from '../../components/form-course/form-course.component';
+
+// Interfaces para el tipado de datos
+interface CourseData {
+ course: {
+ title: string;
+ description: string;
+ goto: string;
+ };
+ contents: ContentData[];
+}
+
+interface ContentData {
+ title: string;
+ paragraph: string[];
+ subcontent: SubcontentData[];
+ next: string | null;
+}
+
+interface SubcontentData {
+ subtitle: string;
+ subparagraph: string[];
+ example: ExampleData[];
+}
+
+interface ExampleData {
+ code: string;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
+}
+
+@Component({
+ selector: 'app-edit-course',
+ standalone: true,
+ templateUrl: './edit-course.component.html',
+ styleUrls: ['./edit-course.component.scss'],
+ imports: [CommonModule, FormCourseComponent]
+})
+export class EditCourseComponent implements OnInit {
+ // Estados del componente
+ isSubmitting = false;
+ isFormValid = false;
+ showPreview = false;
+ isLoading = true;
+ hasError = false;
+ errorMessage = '';
+ hasChanges = false;
+
+ // Datos del curso
+ courseId: string | null = null;
+ initialCourseData: CourseData | undefined = undefined;
+ currentCourseData: CourseData | undefined = undefined;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
+
+ ngOnInit(): void {
+ this.loadCourseData();
+ }
+
+ // Cargar datos del curso desde la ruta
+ private async loadCourseData(): Promise {
+ try {
+ this.isLoading = true;
+ this.hasError = false;
+
+ // Obtener ID del curso desde la ruta
+ this.courseId = this.route.snapshot.paramMap.get('id');
+
+ if (!this.courseId) {
+ throw new Error('ID del curso no encontrado');
+ }
+
+ // AQUI CARGAR LOS DATOS DEL CURSO
+ this.initialCourseData = await this.fetchCourseData(this.courseId);
+ this.currentCourseData = JSON.parse(JSON.stringify(this.initialCourseData)); // Deep copy
+
+ console.log('Datos del curso cargados:', this.initialCourseData);
+
+ } catch (error) {
+ console.error('Error al cargar el curso:', error);
+ this.hasError = true;
+ this.errorMessage = error instanceof Error ? error.message : 'Error desconocido';
+ } finally {
+ this.isLoading = false;
+ }
+ }
+
+ // Simular carga de datos del curso (reemplaza con tu lógica real)
+ private async fetchCourseData(courseId: string): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Simular éxito o error
+ const success = Math.random() > 0.1; // 90% de probabilidad de éxito
+
+ if (success) {
+ // Mock data basado en el formato JSON que proporcionaste
+ const mockCourseData: CourseData = {
+ course: {
+ title: "askfjsafkj",
+ description: "kkjafsfakjkjfskjsfkj",
+ goto: "askfjsafkj"
+ },
+ contents: [
+ {
+ title: "puebads",
+ paragraph: [
+ "ndjkgandga"
+ ],
+ subcontent: [
+ {
+ subtitle: "vkjadkjvnkjad",
+ subparagraph: [
+ "kjadavknjajdknj"
+ ],
+ example: [
+ {
+ code: "djvanjkdvnkja",
+ maxResourceConsumption: 124,
+ maxProcessingTime: 12531
+ },
+ {
+ code: "scas",
+ maxResourceConsumption: 124,
+ maxProcessingTime: 214
+ },
+ {
+ code: "39519",
+ maxResourceConsumption: 8462,
+ maxProcessingTime: 1358
+ }
+ ]
+ },
+ {
+ subtitle: "adkjakjdgkj",
+ subparagraph: [
+ "kdgkjkjag"
+ ],
+ example: [
+ {
+ code: "akgjkajgkj",
+ maxResourceConsumption: 853,
+ maxProcessingTime: 8135
+ }
+ ]
+ }
+ ],
+ next: "asgn"
+ },
+ {
+ title: "asgn",
+ paragraph: [
+ "kdkjkjgnenkj"
+ ],
+ subcontent: [
+ {
+ subtitle: "agkjganjk",
+ subparagraph: [
+ "kjasgsag"
+ ],
+ example: [
+ {
+ code: "15",
+ maxResourceConsumption: 215,
+ maxProcessingTime: 4624
+ }
+ ]
+ }
+ ],
+ next: null
+ }
+ ]
+ };
+ resolve(mockCourseData);
+ } else {
+ reject(new Error('No se pudo cargar el curso'));
+ }
+ }, 1500); // Simular 1.5 segundos de carga
+ });
+ }
+
+ // Manejar cambios en los datos del formulario
+ onFormDataChange(courseData: CourseData): void {
+ this.currentCourseData = courseData;
+ this.checkForChanges();
+ console.log('Datos del curso actualizados:', courseData);
+ }
+
+ // Manejar cambios en la validez del formulario
+ onFormValidChange(isValid: boolean): void {
+ this.isFormValid = isValid;
+ console.log('Estado de validez del formulario:', isValid);
+ }
+
+ // Verificar si hay cambios en el formulario
+ private checkForChanges(): void {
+ if (!this.initialCourseData || !this.currentCourseData) {
+ this.hasChanges = false;
+ return;
+ }
+
+ const initialJson = JSON.stringify(this.initialCourseData);
+ const currentJson = JSON.stringify(this.currentCourseData);
+ this.hasChanges = initialJson !== currentJson;
+ }
+
+ // Alternar vista previa del JSON
+ togglePreview(): void {
+ this.showPreview = !this.showPreview;
+ }
+
+ // Enviar formulario
+ async onSubmit(): Promise {
+ if (!this.isFormValid || !this.currentCourseData || !this.hasChanges) {
+ console.error('Formulario inválido, sin datos o sin cambios');
+ return;
+ }
+
+ this.isSubmitting = true;
+
+ try {
+ console.log('Guardando cambios del curso:', this.currentCourseData);
+
+ // Simular una llamada asíncrona
+ await this.updateCourse(this.courseId!, this.currentCourseData);
+
+ // Actualizar datos iniciales después del guardado exitoso
+ this.initialCourseData = JSON.parse(JSON.stringify(this.currentCourseData));
+ this.hasChanges = false;
+
+ // Mostrar mensaje de éxito
+ alert('¡Curso actualizado exitosamente!');
+
+ // Opcionalmente redirigir
+ // this.router.navigate(['/courses']);
+
+ } catch (error) {
+ console.error('Error al actualizar el curso:', error);
+ alert('Error al actualizar el curso. Por favor, inténtalo de nuevo.');
+ } finally {
+ this.isSubmitting = false;
+ }
+ }
+
+ // Simular actualización del curso (reemplaza con tu lógica real)
+ private async updateCourse(courseId: string, courseData: CourseData): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Simular éxito o error
+ const success = Math.random() > 0.1; // 90% de probabilidad de éxito
+
+ if (success) {
+ resolve();
+ } else {
+ reject(new Error('Error simulado en el servidor'));
+ }
+ }, 2000); // Simular 2 segundos de carga
+ });
+ }
+
+ // Cancelar edición
+ onCancel(): void {
+ if (this.hasChanges) {
+ const confirmLeave = confirm('¿Estás seguro de que quieres cancelar? Se perderán todos los cambios no guardados.');
+ if (!confirmLeave) {
+ return;
+ }
+ }
+
+ this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
+ }
+
+ // Reintentar carga de datos
+ onRetry(): void {
+ this.loadCourseData();
+ }
+
+ // Método para obtener datos del curso (útil para debugging)
+ getCourseData(): CourseData | undefined {
+ return this.currentCourseData;
+ }
+
+ // Verificar si hay cambios pendientes
+ get hasPendingChanges(): boolean {
+ return this.hasChanges;
+ }
+
+ // Obtener estado de carga
+ get isLoadingData(): boolean {
+ return this.isLoading;
+ }
+
+ // Obtener estado de error
+ get hasLoadingError(): boolean {
+ return this.hasError;
+ }
+}
\ No newline at end of file
From d4b59af9c12fe6fc5229aa67e80d4c98a3b11578 Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 21:01:57 -0400
Subject: [PATCH 07/15] campos de espacio y tiempo maximo cambiado
---
.../form-course/form-course.component.html | 66 ++++-----
.../form-course/form-course.component.ts | 61 ++++----
.../create-course/create-course.component.ts | 14 +-
.../edit-course/edit-course.component.ts | 132 ++++++++----------
4 files changed, 128 insertions(+), 145 deletions(-)
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.html b/frontend/angular/src/app/components/form-course/form-course.component.html
index 9a0f3fd..cc7b5cf 100644
--- a/frontend/angular/src/app/components/form-course/form-course.component.html
+++ b/frontend/angular/src/app/components/form-course/form-course.component.html
@@ -85,6 +85,39 @@ 📄 Contenido {{ i + 1 }}
rows="2">
+
+
+
-
-
-
diff --git a/frontend/angular/src/app/components/form-course/form-course.component.ts b/frontend/angular/src/app/components/form-course/form-course.component.ts
index 7d495bb..960ef13 100644
--- a/frontend/angular/src/app/components/form-course/form-course.component.ts
+++ b/frontend/angular/src/app/components/form-course/form-course.component.ts
@@ -16,7 +16,9 @@ interface ContentData {
title: string;
paragraph: string[];
subcontent: SubcontentData[];
- next: string | null;
+ next: string | null;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface SubcontentData {
@@ -27,14 +29,14 @@ interface SubcontentData {
interface ExampleData {
code: string;
- maxResourceConsumption: number;
- maxProcessingTime: number;
}
interface FormContentValue {
title: string;
paragraph: string;
subcontent: FormSubcontentValue[];
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface FormSubcontentValue {
@@ -45,8 +47,6 @@ interface FormSubcontentValue {
interface FormExampleValue {
code: string;
- maxResourceConsumption: number;
- maxProcessingTime: number;
}
interface FormValue {
@@ -119,17 +119,25 @@ export class FormCourseComponent implements OnInit, OnChanges {
});
}
- private createExampleFormGroup(example?: ExampleData): FormGroup {
+ private createContentFormGroup(content?: ContentData): FormGroup {
return this.fb.group({
- code: [example?.code || '', Validators.required],
+ title: [content?.title || '', Validators.required],
+ paragraph: [content?.paragraph?.join('\n') || '', Validators.required],
maxResourceConsumption: [
- example?.maxResourceConsumption || '',
+ content?.maxResourceConsumption || '',
[Validators.required, positiveIntegerValidator]
],
maxProcessingTime: [
- example?.maxProcessingTime || '',
+ content?.maxProcessingTime || '',
[Validators.required, positiveIntegerValidator]
- ]
+ ],
+ subcontent: this.fb.array([])
+ });
+ }
+
+ private createExampleFormGroup(example?: ExampleData): FormGroup {
+ return this.fb.group({
+ code: [example?.code || '', Validators.required]
});
}
@@ -145,11 +153,7 @@ export class FormCourseComponent implements OnInit, OnChanges {
// Cargar contenidos
data.contents.forEach(content => {
- const contentGroup = this.fb.group({
- title: [content.title, Validators.required],
- paragraph: [content.paragraph.join('\n'), Validators.required],
- subcontent: this.fb.array([])
- });
+ const contentGroup = this.createContentFormGroup(content);
// Cargar subcontenidos
content.subcontent.forEach(subcontent => {
@@ -187,12 +191,7 @@ export class FormCourseComponent implements OnInit, OnChanges {
// Métodos para manejar contenidos
addContent(): void {
if (this.contents.length < 20) {
- const contentGroup = this.fb.group({
- title: ['', Validators.required],
- paragraph: ['', Validators.required],
- subcontent: this.fb.array([])
- });
-
+ const contentGroup = this.createContentFormGroup();
this.contents.push(contentGroup);
// Agregar el primer subcontenido automáticamente
@@ -275,12 +274,12 @@ export class FormCourseComponent implements OnInit, OnChanges {
example: sub.example
.filter((ex: FormExampleValue) => ex.code.trim() !== '')
.map((ex: FormExampleValue) => ({
- code: ex.code,
- maxResourceConsumption: Number(ex.maxResourceConsumption),
- maxProcessingTime: Number(ex.maxProcessingTime)
+ code: ex.code
}))
})),
- next: null // Inicializar next como null por defecto
+ next: null, // Inicializar next como null por defecto
+ maxResourceConsumption: Number(content.maxResourceConsumption),
+ maxProcessingTime: Number(content.maxProcessingTime)
};
// Agregar "next" automáticamente si no es el último contenido
@@ -327,9 +326,7 @@ export class FormCourseComponent implements OnInit, OnChanges {
for (let j = 0; j < subcontents.length; j++) {
const examples = this.getExamples(i, j);
const validExamples = examples.controls.filter(ex =>
- ex.get('code')?.value.trim() !== '' &&
- ex.get('maxResourceConsumption')?.valid &&
- ex.get('maxProcessingTime')?.valid
+ ex.get('code')?.value.trim() !== ''
);
if (validExamples.length === 0) {
alert(`El subcontenido ${j + 1} del contenido ${i + 1} debe tener al menos un ejemplo válido`);
@@ -390,8 +387,8 @@ export class FormCourseComponent implements OnInit, OnChanges {
}
// Métodos auxiliares para obtener mensajes de error
- getResourceConsumptionError(contentIndex: number, subcontentIndex: number, exampleIndex: number): string {
- const control = this.getExamples(contentIndex, subcontentIndex).at(exampleIndex).get('maxResourceConsumption');
+ getResourceConsumptionError(contentIndex: number): string {
+ const control = this.contents.at(contentIndex).get('maxResourceConsumption');
if (control?.hasError('required')) {
return 'El consumo máximo de recursos es requerido';
}
@@ -401,8 +398,8 @@ export class FormCourseComponent implements OnInit, OnChanges {
return '';
}
- getProcessingTimeError(contentIndex: number, subcontentIndex: number, exampleIndex: number): string {
- const control = this.getExamples(contentIndex, subcontentIndex).at(exampleIndex).get('maxProcessingTime');
+ getProcessingTimeError(contentIndex: number): string {
+ const control = this.contents.at(contentIndex).get('maxProcessingTime');
if (control?.hasError('required')) {
return 'El tiempo máximo de procesamiento es requerido';
}
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.ts b/frontend/angular/src/app/pages/create-course/create-course.component.ts
index ca33328..a3d59c8 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.ts
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.ts
@@ -17,13 +17,19 @@ interface ContentData {
title: string;
paragraph: string[];
subcontent: SubcontentData[];
- next: string | null;
+ next: string | null;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface SubcontentData {
subtitle: string;
subparagraph: string[];
- example: string[];
+ example: ExampleData[];
+}
+
+interface ExampleData {
+ code: string;
}
@Component({
@@ -78,7 +84,7 @@ export class CreateCourseComponent {
// Mostrar mensaje de éxito y redirigir
alert('¡Curso creado exitosamente!');
- this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
+ this.router.navigate(['/cursos']); // Ajusta la ruta según tu aplicación
} catch (error) {
console.error('Error al crear el curso:', error);
@@ -115,7 +121,7 @@ export class CreateCourseComponent {
}
}
- this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
+ this.router.navigate(['/cursos']); // Ajusta la ruta según tu aplicación
}
// Método para obtener datos del curso (útil para debugging)
diff --git a/frontend/angular/src/app/pages/edit-course/edit-course.component.ts b/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
index 36f69b8..7ed7025 100644
--- a/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
@@ -17,7 +17,9 @@ interface ContentData {
title: string;
paragraph: string[];
subcontent: SubcontentData[];
- next: string | null;
+ next: string | null;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
}
interface SubcontentData {
@@ -28,8 +30,6 @@ interface SubcontentData {
interface ExampleData {
code: string;
- maxResourceConsumption: number;
- maxProcessingTime: number;
}
@Component({
@@ -76,7 +76,7 @@ export class EditCourseComponent implements OnInit {
throw new Error('ID del curso no encontrado');
}
- // AQUI CARGAR LOS DATOS DEL CURSO
+ // Cargar datos del curso
this.initialCourseData = await this.fetchCourseData(this.courseId);
this.currentCourseData = JSON.parse(JSON.stringify(this.initialCourseData)); // Deep copy
@@ -101,81 +101,61 @@ export class EditCourseComponent implements OnInit {
if (success) {
// Mock data basado en el formato JSON que proporcionaste
const mockCourseData: CourseData = {
- course: {
- title: "askfjsafkj",
- description: "kkjafsfakjkjfskjsfkj",
- goto: "askfjsafkj"
- },
- contents: [
+ course: {
+ title: "afsafasfa",
+ description: "safagsgsgsaggas",
+ goto: "afsafasfa"
+ },
+ contents: [
+ {
+ title: "agsgasagsgsag",
+ paragraph: [
+ "asgasgaasg"
+ ],
+ subcontent: [
+ {
+ subtitle: "afavvqeqveqve",
+ subparagraph: [
+ "qvevqevqeqveqve"
+ ],
+ example: [
{
- title: "puebads",
- paragraph: [
- "ndjkgandga"
- ],
- subcontent: [
- {
- subtitle: "vkjadkjvnkjad",
- subparagraph: [
- "kjadavknjajdknj"
- ],
- example: [
- {
- code: "djvanjkdvnkja",
- maxResourceConsumption: 124,
- maxProcessingTime: 12531
- },
- {
- code: "scas",
- maxResourceConsumption: 124,
- maxProcessingTime: 214
- },
- {
- code: "39519",
- maxResourceConsumption: 8462,
- maxProcessingTime: 1358
- }
- ]
- },
- {
- subtitle: "adkjakjdgkj",
- subparagraph: [
- "kdgkjkjag"
- ],
- example: [
- {
- code: "akgjkajgkj",
- maxResourceConsumption: 853,
- maxProcessingTime: 8135
- }
- ]
- }
- ],
- next: "asgn"
+ code: "142124dv"
},
{
- title: "asgn",
- paragraph: [
- "kdkjkjgnenkj"
- ],
- subcontent: [
- {
- subtitle: "agkjganjk",
- subparagraph: [
- "kjasgsag"
- ],
- example: [
- {
- code: "15",
- maxResourceConsumption: 215,
- maxProcessingTime: 4624
- }
- ]
- }
- ],
- next: null
+ code: "dsvsdvdvsd112215"
+ }
+ ]
+ }
+ ],
+ next: "fqefqffqw",
+ maxResourceConsumption: 123,
+ maxProcessingTime: 421
+ },
+ {
+ title: "fqefqffqw",
+ paragraph: [
+ "12wfqfeveqvqqve"
+ ],
+ subcontent: [
+ {
+ subtitle: "qwrwqrqwr",
+ subparagraph: [
+ "qwrwrqqwr"
+ ],
+ example: [
+ {
+ code: "qwrqw"
}
]
- };
+ }
+ ],
+ next: null,
+ maxResourceConsumption: 124,
+ maxProcessingTime: 531
+ }
+ ]
+ };
resolve(mockCourseData);
} else {
reject(new Error('No se pudo cargar el curso'));
@@ -237,7 +217,7 @@ export class EditCourseComponent implements OnInit {
alert('¡Curso actualizado exitosamente!');
// Opcionalmente redirigir
- // this.router.navigate(['/courses']);
+ this.router.navigate(['/cursos']);
} catch (error) {
console.error('Error al actualizar el curso:', error);
@@ -272,7 +252,7 @@ export class EditCourseComponent implements OnInit {
}
}
- this.router.navigate(['/courses']); // Ajusta la ruta según tu aplicación
+ this.router.navigate(['/cursos']); // Ajusta la ruta según tu aplicación
}
// Reintentar carga de datos
From 5be251f77f92ef7484cfe100421039309cb7255d Mon Sep 17 00:00:00 2001
From: jeremiasVA <150405774+jeremiasVA@users.noreply.github.com>
Date: Mon, 23 Jun 2025 21:03:57 -0400
Subject: [PATCH 08/15] Vista previa del JSON eliminado, solo era pa probar
---
.../app/pages/create-course/create-course.component.html | 7 -------
.../src/app/pages/edit-course/edit-course.component.html | 7 -------
2 files changed, 14 deletions(-)
diff --git a/frontend/angular/src/app/pages/create-course/create-course.component.html b/frontend/angular/src/app/pages/create-course/create-course.component.html
index ab93853..750dcf4 100644
--- a/frontend/angular/src/app/pages/create-course/create-course.component.html
+++ b/frontend/angular/src/app/pages/create-course/create-course.component.html
@@ -16,13 +16,6 @@ Crear Nuevo Curso
-