Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Frontend/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "path";
const nextConfig: NextConfig = {
sassOptions: {
includePaths: [path.join(__dirname, "src/styles")],
prependData: `@import "vars.scss";`
prependData: `@use "vars.scss" as *;`
},
/* config options here */
};
Expand Down
23 changes: 7 additions & 16 deletions Frontend/src/app/classes/[class_id]/page.test.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,29 @@
import { renderToString } from "react-dom/server";
import { startTransition } from "react";
import ClassPage from "./page";
import { Class } from "@/types";
import { describe, it, expect, vi } from "vitest";

vi.mock("@/components/VideoPlayer/VideoPlayer", () => ({
VideoPlayer: ({ src, title }: { src: string; title: string }) => (
<div data-testid="mock-video-player">
{title} - {src}
</div>
),
}));

// Mock de fetch que resuelve inmediatamente
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve({
id: 19,
title: "Clase de Test",
name: "Clase de Test",
description: "Descripción de la clase de test",
video: "https://test.com/video.mp4",
duration: 1200,
slug: "clase-test",
} as Class),
});

describe("ClassPage", () => {
it("renders class info and video", async () => {
const html = await renderToString(<ClassPage params={{ class_id: "19" }} />);
it("renders class info", async () => {
const element = await ClassPage({
params: Promise.resolve({ class_id: "19" }),
});
const html = renderToString(element);

expect(html).toContain("Clase de Test");
expect(html).toContain("Descripción de la clase de test");
expect(html).toContain("mock-video-player");
expect(html).toContain("Regresar al curso");
}, 10000); // Aumentamos el timeout a 10 segundos
}, 10000);
});
36 changes: 24 additions & 12 deletions Frontend/src/app/classes/[class_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
import { Class } from "@/types";
import { VideoPlayer } from "@/components/VideoPlayer/VideoPlayer";
import Link from "next/link";
import styles from "./page.module.scss";

interface ClassPageProps {
params: { class_id: string };
params: Promise<{ class_id: string }>;
}

async function getClassData(class_id: string): Promise<Class> {
const res = await fetch(`http://localhost:8000/classes/${class_id}`, { cache: "no-store" });
if (!res.ok) throw new Error("No se pudo cargar la clase");
return res.json();
async function getClassData(class_id: string): Promise<Class | null> {
try {
const res = await fetch(`http://localhost:8000/classes/${class_id}`, { cache: "no-store" });
if (!res.ok) return null;
return res.json();
} catch {
return null;
}
}

export default async function ClassPage({ params }: ClassPageProps) {
const classData = await getClassData(params.class_id);
const { class_id } = await params;
const classData = await getClassData(class_id);

if (!classData) {
return (
<main className={styles.container}>
<p>No se pudo cargar la clase.</p>
<Link href="/" className={styles.backButton}>
← Volver a cursos
</Link>
</main>
);
}

// Asumimos que classData tiene un campo 'slug' para el curso, si no, ajustar aquí
// Si no hay relación directa, el botón puede regresar a /course
return (
<main className={styles.container}>
<VideoPlayer src={classData.video} title={classData.title} />
<h1 className={styles.title}>{classData.title}</h1>
<h1 className={styles.title}>{classData.name}</h1>
<p className={styles.description}>{classData.description}</p>
<Link href="/course" className={styles.backButton}>
<Link href="/" className={styles.backButton}>
← Regresar al curso
</Link>
</main>
Expand Down
1 change: 0 additions & 1 deletion Frontend/src/app/course/[slug]/error.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../../../styles/vars.scss';

.container {
min-height: 100vh;
Expand Down
1 change: 0 additions & 1 deletion Frontend/src/app/course/[slug]/loading.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../../../styles/vars.scss';

.container {
min-height: 100vh;
Expand Down
1 change: 0 additions & 1 deletion Frontend/src/app/course/[slug]/not-found.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../../../styles/vars.scss';

.container {
min-height: 100vh;
Expand Down
12 changes: 6 additions & 6 deletions Frontend/src/app/course/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { CourseDetail } from "@/types";
import { CourseDetailComponent } from "@/components/CourseDetail/CourseDetail";

interface CoursePageProps {
params: {
slug: string;
};
params: Promise<{ slug: string }>;
}

async function getCourseData(slug: string): Promise<CourseDetail> {
Expand All @@ -25,16 +23,18 @@ async function getCourseData(slug: string): Promise<CourseDetail> {
}

export default async function CoursePage({ params }: CoursePageProps) {
const courseData = await getCourseData(params.slug);
const { slug } = await params;
const courseData = await getCourseData(slug);

return <CourseDetailComponent course={courseData} />;
}

export async function generateMetadata({ params }: CoursePageProps) {
const courseData = await getCourseData(params.slug);
const { slug } = await params;
const courseData = await getCourseData(slug);

return {
title: `${courseData.title} - Curso Online`,
title: `${courseData.name} - Curso Online`,
description: courseData.description,
};
}
1 change: 0 additions & 1 deletion Frontend/src/app/page.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../styles/vars.scss';

.page {
min-height: 100vh;
Expand Down
19 changes: 10 additions & 9 deletions Frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import { Course as CourseComponent } from "@/components/Course/Course";
import Link from "next/link";

async function getCourses(): Promise<Course[]> {
const res = await fetch("http://localhost:8000/courses", { cache: "no-store" });
if (!res.ok) {
throw new Error("Failed to fetch courses");
try {
const res = await fetch("http://localhost:8000/courses", { cache: "no-store" });
if (!res.ok) {
throw new Error("Failed to fetch courses");
}
return await res.json();
} catch {
return [];
}
const data = await res.json();
return data.data;
}

export default async function Home() {
Expand All @@ -32,10 +35,8 @@ export default async function Home() {
{courses.map((course) => (
<Link href={`/course/${course.slug}`} key={course.id}>
<CourseComponent
id={course.id}
title={course.title}
teacher={course.teacher}
duration={course.duration}
name={course.name}
description={course.description}
thumbnail={course.thumbnail}
/>
</Link>
Expand Down
1 change: 0 additions & 1 deletion Frontend/src/components/Course/Course.module.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../../styles/vars.scss';

.courseCard {
background: color('white');
Expand Down
9 changes: 4 additions & 5 deletions Frontend/src/components/Course/Course.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import { Course as CourseType } from "@/types";

type CourseProps = Omit<CourseType, "slug">;

export const Course = ({ id, title, teacher, duration, thumbnail }: CourseProps) => {
export const Course = ({ name, description, thumbnail }: Omit<CourseProps, "id">) => {
return (
<article className={styles.courseCard}>
<div className={styles.thumbnailContainer}>
<img src={thumbnail} alt={title} className={styles.thumbnail} />
<img src={thumbnail} alt={name} className={styles.thumbnail} />
</div>
<div className={styles.courseInfo}>
<h2 className={styles.courseTitle}>{title}</h2>
<p className={styles.teacher}>Profesor: {teacher}</p>
<p className={styles.duration}>Duración: {duration} minutos</p>
<h2 className={styles.courseTitle}>{name}</h2>
<p className={styles.description}>{description}</p>
</div>
</article>
);
Expand Down
23 changes: 5 additions & 18 deletions Frontend/src/components/Course/__test__/Course.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,31 @@ import { Course } from "../Course";

describe("Course Component", () => {
const mockCourse = {
id: 1,
title: "React Fundamentals",
teacher: "John Doe",
duration: 120,
name: "React Fundamentals",
description: "Aprende React desde cero",
thumbnail: "https://example.com/thumbnail.jpg",
};

it("renders course information correctly", () => {
render(<Course {...mockCourse} />);

// Check if title is rendered
expect(screen.getByText(mockCourse.title)).toBeDefined();

// Check if teacher information is rendered
expect(screen.getByText(`Profesor: ${mockCourse.teacher}`)).toBeDefined();

// Check if duration is rendered
expect(screen.getByText(`Duración: ${mockCourse.duration} minutos`)).toBeDefined();
expect(screen.getByText(mockCourse.name)).toBeDefined();
expect(screen.getByText(mockCourse.description)).toBeDefined();
});

it("renders thumbnail with correct alt text", () => {
render(<Course {...mockCourse} />);

const thumbnail = screen.getByRole("img");
expect(thumbnail).toHaveAttribute("src", mockCourse.thumbnail);
expect(thumbnail).toHaveAttribute("alt", mockCourse.title);
expect(thumbnail).toHaveAttribute("alt", mockCourse.name);
});

it("renders with correct structure", () => {
const { container } = render(<Course {...mockCourse} />);

// Check if the main article exists
expect(container.querySelector("article")).toBeDefined();

// Check if the thumbnail container exists
expect(container.querySelector("div > img")).toBeDefined();

// Check if the course info section exists
expect(container.querySelector("div > h2")).toBeDefined();
expect(container.querySelector("div > p")).toBeDefined();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
@import '../../styles/vars.scss';

.container {
max-width: 1200px;
Expand Down
21 changes: 5 additions & 16 deletions Frontend/src/components/CourseDetail/CourseDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ interface CourseDetailComponentProps {
}

export const CourseDetailComponent: FC<CourseDetailComponentProps> = ({ course }) => {
const formatDuration = (duration: number) => {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
return `${hours}h ${minutes}m`;
};

const totalDuration = course.classes.reduce((acc, cls) => acc + cls.duration, 0);

return (
<div className={styles.container}>
<div className={styles.navigation}>
Expand All @@ -25,14 +17,12 @@ export const CourseDetailComponent: FC<CourseDetailComponentProps> = ({ course }
</div>
<div className={styles.header}>
<div className={styles.thumbnailContainer}>
<img src={course.thumbnail} alt={course.title} className={styles.thumbnail} />
<img src={course.thumbnail} alt={course.name} className={styles.thumbnail} />
</div>
<div className={styles.courseInfo}>
<h1 className={styles.title}>{course.title}</h1>
<p className={styles.teacher}>Por {course.teacher}</p>
<h1 className={styles.title}>{course.name}</h1>
<p className={styles.description}>{course.description}</p>
<div className={styles.stats}>
<span className={styles.duration}>Duración total: {formatDuration(totalDuration)}</span>
<span className={styles.classCount}>{course.classes.length} clases</span>
</div>
</div>
Expand All @@ -42,14 +32,13 @@ export const CourseDetailComponent: FC<CourseDetailComponentProps> = ({ course }
<h2 className={styles.sectionTitle}>Contenido del curso</h2>
<div className={styles.classesList}>
{course.classes.map((cls, index) => (
<Link href={`/classes/${cls.id}`} key={cls.id} className={styles.classItem}>
<div key={cls.id} className={styles.classItem}>
<div className={styles.classNumber}>{(index + 1).toString().padStart(2, "0")}</div>
<div className={styles.classInfo}>
<h3 className={styles.classTitle}>{cls.title}</h3>
<h3 className={styles.classTitle}>{cls.name}</h3>
<p className={styles.classDescription}>{cls.description}</p>
<span className={styles.classDuration}>{formatDuration(cls.duration)}</span>
</div>
</Link>
</div>
))}
</div>
</div>
Expand Down
4 changes: 3 additions & 1 deletion Frontend/src/styles/vars.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@use "sass:map";

// Color Tokens
$colors: (
// Primary Colors
Expand All @@ -17,7 +19,7 @@ $colors: (

// Function to get color values
@function color($color-name) {
@return map-get($colors, $color-name);
@return map.get($colors, $color-name);
}

// Usage example:
Expand Down
11 changes: 4 additions & 7 deletions Frontend/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
// Course types
export interface Course {
id: number;
title: string;
teacher: string;
duration: number;
name: string;
description: string;
thumbnail: string;
slug: string;
}

// Class types
export interface Class {
id: number;
title: string;
name: string;
description: string;
video: string;
duration: number;
slug: string;
}

// Course Detail type
export interface CourseDetail extends Course {
description: string;
teacher_id: number[];
classes: Class[];
}

Expand Down