-
+
@for (curso of cursos; track curso.id) {
-
}
diff --git a/frontend/angular/src/app/pages/cursos/cursos.component.scss b/frontend/angular/src/app/pages/cursos/cursos.component.scss
index 026160b..fb51d4c 100644
--- a/frontend/angular/src/app/pages/cursos/cursos.component.scss
+++ b/frontend/angular/src/app/pages/cursos/cursos.component.scss
@@ -1,18 +1,13 @@
.cursos-container {
- height: 100%;
- width: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
+ padding: 40px 20px;
+ max-width: 1200px;
+ margin: 0 auto;
}
-.cards-container {
- width: 50%;
+.cards-grid {
display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- grid-auto-rows: 150px;
- row-gap: 1rem;
- column-gap: 1rem;
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+ gap: 24px;
}
.single-card :hover {
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..b081525
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.html
@@ -0,0 +1,31 @@
+
+
+
+
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..35e928b
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.spec.ts
@@ -0,0 +1,37 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Router, ActivatedRoute } from '@angular/router';
+
+import { EditCourseComponent } from './edit-course.component';
+
+describe('EditCourseComponent', () => {
+ let component: EditCourseComponent;
+ let fixture: ComponentFixture
;
+
+ beforeEach(async () => {
+ const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
+ const activatedRouteSpy = {
+ snapshot: {
+ paramMap: {
+ get: jasmine.createSpy('get').and.returnValue('123')
+ }
+ }
+ };
+
+ await TestBed.configureTestingModule({
+ imports: [EditCourseComponent],
+ providers: [
+ { provide: Router, useValue: routerSpy },
+ { provide: ActivatedRoute, useValue: activatedRouteSpy }
+ ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(EditCourseComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
\ No newline at end of file
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..fb7ab38
--- /dev/null
+++ b/frontend/angular/src/app/pages/edit-course/edit-course.component.ts
@@ -0,0 +1,273 @@
+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;
+ maxResourceConsumption: number;
+ maxProcessingTime: number;
+}
+
+interface SubcontentData {
+ subtitle: string;
+ subparagraph: string[];
+ example: ExampleData[];
+}
+
+interface ExampleData {
+ code: string;
+}
+
+@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');
+ }
+
+ // Cargar datos del curso
+ this.initialCourseData = await this.fetchCourseData(this.courseId);
+ this.currentCourseData = JSON.parse(JSON.stringify(this.initialCourseData)); // Deep copy
+
+
+ } 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;
+ }
+ }
+
+ private async fetchCourseData(courseId: string): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Simular éxito o error
+ const success = Math.random() > 0.1;
+
+ if (success) {
+ // Mock data basado en el formato JSON
+ const mockCourseData: CourseData = {
+ course: {
+ title: "afsafasfa",
+ description: "safagsgsgsaggas",
+ goto: "afsafasfa"
+ },
+ contents: [
+ {
+ title: "agsgasagsgsag",
+ paragraph: [
+ "asgasgaasg"
+ ],
+ subcontent: [
+ {
+ subtitle: "afavvqeqveqve",
+ subparagraph: [
+ "qvevqevqeqveqve"
+ ],
+ example: [
+ {
+ code: "142124dv"
+ },
+ {
+ 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'));
+ }
+ }, 1500); // Simular 1.5 segundos de carga
+ });
+ }
+
+ // Manejar cambios en los datos del formulario
+ onFormDataChange(courseData: CourseData): void {
+ this.currentCourseData = courseData;
+ this.checkForChanges();
+ }
+
+ // Manejar cambios en la validez del formulario
+ onFormValidChange(isValid: boolean): void {
+ this.isFormValid = 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 {
+ // 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;
+
+ alert('¡Curso actualizado exitosamente!');
+ this.router.navigate(['/cursos']);
+
+ } 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
+ private async updateCourse(courseId: string, courseData: CourseData): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ // Simular éxito o error
+ const success = Math.random() > 0.1;
+
+ 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(['/cursos']);
+ }
+
+ // Reintentar carga de datos
+ onRetry(): void {
+ this.loadCourseData();
+ }
+
+ // Método para obtener datos del curso pa 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
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 }}