Skip to content

opencodigos/Projeto3-PlataformaCursoSimples

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 

Repository files navigation

Projeto 3 - Platadorma de Curso (Simples)

Sistema de Ensino Online

  • Descrição: Crie uma plataforma de aprendizado online onde usuários podem se inscrever em cursos, assistir a vídeos, baixar materiais.

Link do Projeto Inicial

Link da Playlist para Acompanhar tutorial)

📀 Server

Instalar as Dependencias

Django django-cors-headers djangorestframework djangorestframework-simplejwt Pillow

Configurações dos Modelos

from django.db import models
from django.contrib.auth.models import User

class Curso(models.Model):
    """
    Informações públicas do curso
    """
    nome = models.CharField(max_length=200)
    descricao = models.TextField()
    imagem = models.ImageField(upload_to='cursos/', blank=True, null=True)
    ativo = models.BooleanField(default=True)
    criado_em = models.DateTimeField(auto_now_add=True)
    atualizado_em = models.DateTimeField(auto_now=True)
    
    class Meta:
        verbose_name = 'Curso'
        verbose_name_plural = 'Cursos'
        ordering = ['-criado_em']
    
    def __str__(self):
        return self.nome

class Secao(models.Model):
    """
    Seções/Tópicos do curso
    """
    curso = models.ForeignKey(Curso, on_delete=models.CASCADE, related_name='secoes')
    titulo = models.CharField(max_length=200)
    descricao = models.TextField(blank=True)
    ordem = models.IntegerField(default=0)
    
    class Meta:
        verbose_name = 'Seção'
        verbose_name_plural = 'Seções'
        ordering = ['ordem']
    
    def __str__(self):
        return f"{self.curso.nome} - {self.titulo}"

class Aula(models.Model):
    """
    Aulas com vídeos e materiais
    """
    secao = models.ForeignKey(Secao, on_delete=models.CASCADE, related_name='aulas')
    titulo = models.CharField(max_length=200)
    descricao = models.TextField(blank=True)
    video_url = models.URLField(help_text='Link externo do vídeo (YouTube, Vimeo, etc)')
    ordem = models.IntegerField(default=0)
    duracao = models.CharField(max_length=20, blank=True, help_text='Ex: 15:30')
    
    class Meta:
        verbose_name = 'Aula'
        verbose_name_plural = 'Aulas'
        ordering = ['ordem']
    
    def __str__(self):
        return f"{self.secao.titulo} - {self.titulo}"

class Material(models.Model):
    """
    Materiais para download de cada aula
    """
    aula = models.ForeignKey(Aula, on_delete=models.CASCADE, related_name='materiais')
    titulo = models.CharField(max_length=200)
    arquivo = models.FileField(upload_to='materiais/', blank=True, null=True)
    arquivo_url = models.URLField(blank=True, null=True)
    tipo = models.CharField(max_length=50, blank=True, help_text='PDF, DOC, etc')
    
    class Meta:
        verbose_name = 'Material'
        verbose_name_plural = 'Materiais'
    
    def __str__(self):
        return f"{self.aula.titulo} - {self.titulo}"

class Inscricao(models.Model):
    """
    Relaciona usuário com curso
    """
    usuario = models.ForeignKey(User, on_delete=models.CASCADE, related_name='inscricoes')
    curso = models.ForeignKey(Curso, on_delete=models.CASCADE, related_name='inscricoes')
    data_inscricao = models.DateTimeField(auto_now_add=True)
    ativo = models.BooleanField(default=True)
    
    class Meta:
        verbose_name = 'Inscrição'
        verbose_name_plural = 'Inscrições'
        unique_together = ['usuario', 'curso']
        ordering = ['-data_inscricao']
    
    def __str__(self):
        return f"{self.usuario.username} - {self.curso.nome}"

Admin

from django.contrib import admin
from .models import Curso, Secao, Aula, Material, Inscricao

class SecaoInline(admin.StackedInline):
    model = Secao
    extra = 1

class AulaInline(admin.StackedInline):
    model = Aula
    extra = 1

class MaterialInline(admin.StackedInline):
    model = Material
    extra = 1

@admin.register(Curso)
class CursoAdmin(admin.ModelAdmin):
    list_display = ['nome', 'ativo', 'criado_em']
    list_filter = ['ativo', 'criado_em']
    search_fields = ['nome', 'descricao']
    inlines = [SecaoInline]

@admin.register(Secao)
class SecaoAdmin(admin.ModelAdmin):
    list_display = ['titulo', 'curso', 'ordem']
    list_filter = ['curso']
    search_fields = ['titulo']
    inlines = [AulaInline]

@admin.register(Aula)
class AulaAdmin(admin.ModelAdmin):
    list_display = ['titulo', 'secao', 'ordem', 'duracao']
    list_filter = ['secao__curso']
    search_fields = ['titulo']
    inlines = [MaterialInline]

@admin.register(Material)
class MaterialAdmin(admin.ModelAdmin):
    list_display = ['titulo', 'aula', 'tipo']
    list_filter = ['tipo']
    search_fields = ['titulo']

@admin.register(Inscricao)
class InscricaoAdmin(admin.ModelAdmin):
    list_display = ['usuario', 'curso', 'data_inscricao', 'ativo']
    list_filter = ['ativo', 'data_inscricao', 'curso']
    search_fields = ['usuario__username', 'curso__nome']

Configurar CORS e JWT

ALLOWED_HOSTS = [
    'localhost',
    '127.0.0.1'
]

# Permite requisições de origens diferentes
CORS_ALLOWED_ORIGINS = [
    'http://localhost:4200',
    'http://127.0.0.1:4200',
]

# Permite que o frontend envie credenciais (cookies, etc.)
CORS_ALLOW_CREDENTIALS = True 

INSTALLED_APPS

'rest_framework',

'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist',

 'corsheaders',

REST E JWT

# Django REST Framework 
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        'rest_framework.authentication.SessionAuthentication', # Adiciona autenticação por sessão
    ),
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticatedOrReadOnly',
    ],
}

# JWT Settings
from datetime import timedelta

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(hours=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': True,
} 

Configurar APIs de Autenticação

auth

from rest_framework import serializers
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.tokens import RefreshToken

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    """
    Serializa o usuário
    """
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'first_name', 'last_name']
        read_only_fields = ['id']
        
# Personalize as reivindicações de token      
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
    @classmethod
    def get_token(cls, user):
        """
        Adiciona dados extras no token JWT
        """
        token = super().get_token(user)
        # Adiciona username no payload do token
        token['username'] = user.username
        return token
    
    def validate(self, attrs):
        """
        Retorna os tokens + dados do usuário
        """
        data = super().validate(attrs)
        
        # Adiciona dados do usuário na resposta
        data['user'] = {
            'id': self.user.id,
            'username': self.user.username,
            'email': self.user.email,
            'first_name': self.user.first_name,
            'last_name': self.user.last_name,
        }
        
        return data

class MyTokenObtainPairView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

class MyTokenRefreshView(TokenRefreshView):
	pass

class LogoutView(APIView):
    permission_classes = (IsAuthenticated,)

    def post(self, request):
        """
        Logout do usuário
        """
        try:
            refresh_token = request.data["refresh"]
            token = RefreshToken(refresh_token)
            token.blacklist() # Adiciona o refresh token à blacklist

            return Response(status=status.HTTP_205_RESET_CONTENT)
        except Exception as e:
            print("[LOGOUT]", e)
            return Response(status=status.HTTP_400_BAD_REQUEST)

Urls

from django.urls import path, include
from rest_framework.routers import DefaultRouter
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView 
from .auth import MyTokenObtainPairView, LogoutView

# Router para ViewSets
router = DefaultRouter() 

urlpatterns = [  
    # API
    path('api/', include(router.urls)),
    
    path('api/login/', MyTokenObtainPairView.as_view(), name='token_obtain_pair'), 
    path('api/logout/', LogoutView.as_view(), name='token_logout'),
    
    # JWT Authentication
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]

Certo Configurações de Autenticação com JWT está feita. Depois vamos configurar no frontend.

Conseguimos testar.

python manage.py runserver

Rota: 127.0.0.1:8000/api

Configurar Serializers e Views dos modelos

Vamos serializar os dados de nossos modelos.

from rest_framework import serializers
from .models import Curso, Secao, Aula, Material, Inscricao  

class MaterialSerializer(serializers.ModelSerializer):
    """
    Serializa o material
    """
    class Meta:
        model = Material
        fields = ['id', 'titulo', 'arquivo_url', 'arquivo', 'tipo']

class AulaSerializer(serializers.ModelSerializer):
    """
    Serializa a aula
    """
    materiais = MaterialSerializer(many=True, read_only=True)
    
    class Meta:
        model = Aula
        fields = ['id', 'titulo', 'descricao', 'video_url', 'ordem', 'duracao', 'materiais']

class SecaoSerializer(serializers.ModelSerializer):
    """
    Serializa a seção
    """
    aulas = AulaSerializer(many=True, read_only=True)
    
    class Meta:
        model = Secao
        fields = ['id', 'titulo', 'descricao', 'ordem', 'aulas']

class CursoListSerializer(serializers.ModelSerializer):
    """
    Serializa lista de cursos
    """
    class Meta:
        model = Curso
        fields = ['id', 'nome', 'descricao', 'imagem', 'criado_em']

Views

from django.shortcuts import render
from rest_framework import viewsets 
from .models import Curso
from .serializers import ( 
    CursoListSerializer
) 

class CursoViewSet(viewsets.ModelViewSet):
    """
    API de Cursos
    - GET /api/cursos/ -> Lista todos os cursos (público) 
    """
    queryset = Curso.objects.all()
    serializer_class = CursoListSerializer
    
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(ativo=True)

Urls

from .views import CursoViewSet 

router.register(r'cursos', CursoViewSet, basename='curso')

Testa.

Vamos configurar endpoint para Detalhes

1 - Quando usuário é publico e precisa ser as informações sobre o curso

class AulaPreviewSerializer(serializers.ModelSerializer):
    """
    Serializa aula SEM vídeo e materiais (preview público)
    """
    class Meta:
        model = Aula
        fields = ['id', 'titulo', 'ordem', 'duracao']

class SecaoPreviewSerializer(serializers.ModelSerializer):
    """
    Serializa seção SEM vídeos e materiais (preview público)
    """
    aulas = AulaPreviewSerializer(many=True, read_only=True)
    
    class Meta:
        model = Secao
        fields = ['id', 'titulo', 'descricao', 'ordem', 'aulas']

class CursoPreviewSerializer(serializers.ModelSerializer):
    """
    Preview público do curso - mostra estrutura SEM vídeos/arquivos
    """
    secoes = SecaoPreviewSerializer(many=True, read_only=True)
    total_secoes = serializers.SerializerMethodField()
    total_aulas = serializers.SerializerMethodField()
    
    class Meta:
        model = Curso
        fields = ['id', 'nome', 'descricao', 'imagem', 'criado_em', 
                  'secoes', 'total_secoes', 'total_aulas']
    
    def get_total_secoes(self, obj):
        return obj.secoes.count()
    
    def get_total_aulas(self, obj):
        return sum(secao.aulas.count() for secao in obj.secoes.all())

2 - Quando usuário é inscrito mostra informações completas do curso

class CursoDetailSerializer(serializers.ModelSerializer):
    """
    Serializa o curso detalhado do curso (apenas para inscritos)
    """
    secoes = SecaoSerializer(many=True, read_only=True)
    total_secoes = serializers.SerializerMethodField()
    total_aulas = serializers.SerializerMethodField()
    
    class Meta:
        model = Curso
        fields = ['id', 'nome', 'descricao', 'imagem', 'criado_em', 
                  'secoes', 'total_secoes', 'total_aulas']
    
    def get_total_secoes(self, obj):
        return obj.secoes.count()
    
    def get_total_aulas(self, obj):
        return sum(secao.aulas.count() for secao in obj.secoes.all())

Views

from django.shortcuts import render
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny 
from .models import Curso, Secao, Aula, Material, Inscricao
from .serializers import ( 
    CursoListSerializer,
    CursoPreviewSerializer,
    CursoDetailSerializer, 
    SecaoSerializer, 
    AulaSerializer, 
    MaterialSerializer,
    InscricaoSerializer
) 

class CursoViewSet(viewsets.ModelViewSet):
    """
    API de Cursos
    - GET /api/cursos/ -> Lista todos os cursos (público)
    - GET /api/cursos/{id}/ -> Detalhes do curso (apenas inscritos)
    - POST /api/cursos/{id}/inscrever/ -> Inscrever no curso
    """
    queryset = Curso.objects.all()
    serializer_class = CursoDetailSerializer
    
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(ativo=True)
        
    # Se for uma lista mostra serializer List 
    # Se não mostra serializer Detail    
    def get_serializer_class(self):
        if self.action == 'list':
            return CursoListSerializer
        return CursoDetailSerializer
    
    # Cria permissão para list e retrieve são publicas 
    # e restante precisa de autenticação
    def get_permissions(self):
        # Listagem e preview são públicos
        if self.action in ['list', 'retrieve']:
            return [AllowAny()]
        return [IsAuthenticated()]
    
    # quando acessa objeto individualmente
    def retrieve(self, request, *args, **kwargs):
        """
        Preview público do curso - mostra estrutura SEM vídeos/arquivos
        Qualquer pessoa pode ver
        """
        curso = self.get_object()
        serializer = CursoPreviewSerializer(curso)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'], permission_classes=[IsAuthenticated])
    def conteudo(self, request, pk=None):
        """
        Conteúdo completo do curso - COM vídeos e arquivos
        Apenas para USUÁRIOS INSCRITOS
        """
        curso = self.get_object()
        
        # Verifica se o usuário está inscrito
        if not Inscricao.objects.filter(
            usuario=request.user, 
            curso=curso, 
            ativo=True).exists():
            return Response(
                {'detail': 'Você precisa estar inscrito neste curso para acessar o conteúdo completo.'},
                status=status.HTTP_403_FORBIDDEN
            )
        
        serializer = CursoDetailSerializer(curso)
        return Response(serializer.data)

Aciton - para Inscrever usuários no curso

    @action(detail=True, methods=['post'], permission_classes=[IsAuthenticated])
    def inscrever(self, request, pk=None):
        """Inscrever usuário no curso"""
        curso = self.get_object()
        
        # Verifica se  está inscrito
        inscricao, created = Inscricao.objects.get_or_create(
            usuario=request.user,
            curso=curso,
            defaults={'ativo': True}
        )
        
        if not created:
            if inscricao.ativo:
                return Response(
                    {'detail': 'Você já está inscrito neste curso.'},
                    status=status.HTTP_400_BAD_REQUEST
                )
            else:
                # Reativa inscrição
                inscricao.ativo = True
                inscricao.save()
        
        serializer = InscricaoSerializer(inscricao)
        return Response(serializer.data, status=status.HTTP_201_CREATED)

Lista de Cursos Ativos relacionados com Usuário (Talvez eu use)

@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def meus_cursos(self, request):
    """Lista cursos em que o usuário está inscrito"""
    inscricoes = Inscricao.objects.filter(usuario=request.user, ativo=True)
    cursos = [inscricao.curso for inscricao in inscricoes]
    serializer = CursoListSerializer(cursos, many=True)
    return Response(serializer.data)

Lista de Cursos que usuários se inscreveu

class InscricaoSerializer(serializers.ModelSerializer):
    """
    Serializa a inscrição do usuário no curso
    """
    curso_nome = serializers.CharField(source='curso.nome', read_only=True)
    usuario_nome = serializers.CharField(source='usuario.username', read_only=True)
    
    class Meta:
        model = Inscricao
        fields = ['id', 'curso', 'curso_nome', 'usuario', 'usuario_nome', 
                  'data_inscricao', 'ativo']
        read_only_fields = ['usuario', 'data_inscricao']
class InscricaoViewSet(viewsets.ModelViewSet):
    """
    API de Inscrições
    - GET /api/inscricoes/ -> Lista inscrições do usuário
    """
    queryset = Inscricao.objects.all()
    serializer_class = InscricaoSerializer
    permission_classes = [IsAuthenticated]
    
    def get_queryset(self):
        qs = super().get_queryset()
        return qs.filter(
            usuario=self.request.user,
            ativo=True
        )
router.register(r'inscricoes', InscricaoViewSet, basename='inscricao')
💻 Client

Vou fazer com Angular.

ng new myapp

SSR = render no servidor zone.js = detecção automática de mudanças Zoneless = sem zone.js (controle manual, mais performático)

Primeiro passo,

Listar os cursos, como é uma api publica

api.ts Vamos criar um serviço de API

Este serviço será responsável por buscar os cursos na nossa API Django.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Curso, CursoDetalhado, Inscricao } from '../models/curso.model';

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private apiUrl = 'http://127.0.0.1:8000/api';

  constructor(private http: HttpClient) {}

  // Cursos
  getCursos(): Observable<Curso[]> {
    return this.http.get<Curso[]>(`${this.apiUrl}/cursos/`);
  }
}

Tipagem dos dados Aqui definimos como os dados virão da API, facilitando o autocomplete e evitando erros no código.

export interface Curso {
  id: number;
  nome: string;
  descricao: string;
  imagem: string | null;
  criado_em: string;
}

Vamos criar um componente.

components>curso-lista.ts

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router'; 
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { ApiService } from '../../services/api.service';
import { Curso } from '../../models/curso.model';

@Component({
  selector: 'app-cursos',
  standalone: true,
  imports: [CommonModule, MatProgressSpinnerModule],
  templateUrl: './curso-lista.html',
  styleUrls: ['./curso-lista.scss']
})
export class CursoLista implements OnInit {
  cursos: Curso[] = [];
  loading = true;
  error = ''; 

  constructor(
    private apiService: ApiService, 
    private router: Router
  ) {}

  ngOnInit(): void {    
    this.loadCursos(); 
  }

  loadCursos(): void {  
    this.apiService.getCursos().subscribe({
      next: (data) => {
        this.cursos = data;
        this.loading = false;
      },
      error: (err) => {
        this.error = 'Erro ao carregar cursos';
        this.loading = false;
        console.error('Erro:', err);
      }
    });
  } 
}

Para usar o mat-spinner

npm install @angular/material @angular/cdk @angular/animations

curso-lista.html

<div class="loading" *ngIf="loading">
  <mat-spinner></mat-spinner>
  <p>Carregando cursos...</p>
</div>

<h2 *ngIf="!loading">Cursos Disponíveis</h2>

<div class="grid" *ngIf="!loading">
  @for (curso of cursos; track curso.id) {

    <div class="card" (click)="verCurso(curso.id)">

      @if (curso.imagem) {
        <img 
          [src]="curso.imagem" 
          [alt]="curso.nome" />
      } @else {
        <img 
          [src]="'https://placehold.co/400'" 
          [alt]="curso.nome" />
      }

      <div class="info">
        <h3>{{ curso.nome }}</h3>
        <p>{{ curso.descricao }}</p>
        <span>Ver Curso →</span>
      </div>
    </div>
  }
</div>

scss

.grid {
  display: grid;
  gap: 1.5rem;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}

.card {
  background: #fff;
  border-radius: 0.8rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  cursor: pointer;
  transition: 0.3s;

  &:hover {
    transform: translateY(-3px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  }

  img {
    width: 100%;
    height: 180px;
    object-fit: cover;
  }

  .info {
    padding: 1rem;

    h3 {
      margin-bottom: 0.5rem;
    }

    p {
      font-size: 0.9rem;
      margin-bottom: 0.5rem;
    }

    span {
      color: #667eea;
      font-weight: 600;
      font-size: 0.9rem;
    }
  }
}

Vamos registrar uma rota para conseguimos testar

import { Routes } from '@angular/router'; 
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  {
    path: '',
    redirectTo: '/cursos',
    pathMatch: 'full'
  }, 
  {
    path: 'cursos',
    loadComponent: () => import('./components/curso-lista/curso-lista').then(m => m.CursoLista)
  }
];

styles.scss

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  background: #f5f7fa;
  color: #333;
  line-height: 1.5;
}

h1, h2, h3, h4, h5, h6 {
  color: #667eea;
  margin-bottom: 0.5rem;
}

p {
  color: #666;
  font-size: 0.95rem;
}
 
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1.5rem;
}

button {
  padding: 0.6rem 1rem;
  border: none;
  border-radius: 0.4rem;
  cursor: pointer;
  font-weight: 500;
  transition: 0.2s;
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;  
  p {
    margin-top: 1rem;
    color: #666;
  }
}

app.html

<router-outlet></router-outlet>

testa

localhost:4200

Podemos ver os detalhes do curso

api.ts

// Preview público (sem vídeos/arquivos)
getCursoPreview(id: number): Observable<CursoDetalhado> {
  return this.http.get<CursoDetalhado>(`${this.apiUrl}/cursos/${id}/`);
}

models>curso.models.ts

export interface Material {
  id: number;
  titulo: string;
  arquivo: string;
  tipo: string;
}

export interface Aula {
  id: number;
  titulo: string;
  descricao: string;
  video_url: string;
  ordem: number;
  duracao: string;
  materiais: Material[];
}

export interface Secao {
  id: number;
  titulo: string;
  descricao: string;
  ordem: number;
  aulas: Aula[];
}

export interface CursoDetalhado extends Curso {
  secoes: Secao[];
  total_secoes: number;
  total_aulas: number;
}

courso-detalhes.ts

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-curso-detalhes',
  standalone: true,
  imports: [
    CommonModule,
  ],
  templateUrl: './curso-detalhes.html',
  styleUrls: ['./curso-detalhes.scss']
})
export class CursoDetalhes implements OnInit {
  curso: CursoDetalhado | null = null;
  loading = true;
  error = '';
  isPreview = true;

  constructor(
    private route: ActivatedRoute,
    public router: Router,
    private apiService: ApiService
  ) {}

  ngOnInit(): void {
    const id = this.route.snapshot.paramMap.get('id');
    if (id) {
      this.loadPreview(id);
    }
  }

  loadPreview(id: number): void {
    this.apiService.getCursoPreview(id).subscribe({
      next: (data) => {
        this.curso = data;
        this.isPreview = true;
        this.loading = false;
      },
      error: (err) => {
        this.loading = false;
        this.error = 'Erro ao carregar curso';
        console.error('Erro:', err);
      }
    });
  }
}

curso-detalhes.scss

.preview-layout,
.inscrito-layout {
  display: grid;
  grid-template-columns: 1fr 350px;
  gap: 2rem;
  margin-top: 2rem;

  .main-content {
    background: #fff;
    padding: 2rem;
    border-radius: 0.8rem;
  }

  .sidebar {
    background: #fff;
    padding: 1.5rem;
    border-radius: 0.8rem;
    position: sticky;
    top: 2rem;
    max-height: calc(100vh - 4rem);
    overflow-y: auto;

    h3 {
      margin-bottom: 1rem;
      color: #667eea;
    }
  }
}

// Preview específico
.preview-layout {
  .stats {
    display: flex;
    gap: 1rem;
    margin: 1rem 0;

    span {
      background: #f0f0f0;
      padding: 0.5rem 1rem;
      border-radius: 1rem;
      font-size: 0.9rem;
    }
  }

  .secoes-preview {
    margin-top: 2rem;

    .secao {
      margin-bottom: 1.5rem;
      padding: 1rem;
      background: #f9f9f9;
      border-radius: 0.5rem;

      h4 {
        color: #667eea;
        margin-bottom: 0.5rem;
      }
    }
  }

  .cta-box {
    text-align: center;

    button {
      width: 100%;
      padding: 1rem;
      background: #667eea;
      color: #fff;
      border: none;
      border-radius: 0.5rem;
      font-weight: 600;
      cursor: pointer;
      transition: 0.2s;
      margin-bottom: 0.5rem;

      &:hover:not(:disabled) {
        background: #5568d3;
      }

      &:disabled {
        background: #ccc;
        cursor: not-allowed;
      }
    }
  }
}

curso-detalhes.html

<div *ngIf="loading" class="loading">Carregando...</div>
<div *ngIf="error" class="error">{{ error }}</div>

<!-- PREVIEW -->
<div *ngIf="curso && isPreview" class="preview-layout">

  <div class="main-content">

    <h2>{{ curso.nome }}</h2>
    <p>{{ curso.descricao }}</p>
    
    <div class="stats">
      <span>{{ curso.total_secoes }} seções</span>
      <span>{{ curso.total_aulas }} aulas</span>
    </div>

    <div class="secoes-preview">
      <h3>Conteúdo do Curso</h3>
      <div class="secao" *ngFor="let secao of curso.secoes">
        <h4>{{ secao.titulo }}</h4>
        @for (aula of secao.aulas; track aula.id; let idx = $index) {
          <p>Aula {{ idx + 1 }}: {{ aula.titulo }}</p>
        } 
      </div>
    </div>

  </div>

  <aside class="sidebar cta-box">

    <h3>Inscreva-se agora!</h3>
    <p>Acesse todos os vídeos e materiais</p>

    <button>
      Inscrever-se
    </button>
    <button>
      Fazer Login para Inscrever
    </button>

  </aside>
</div>

routers

  {
    path: 'curso-detalhes/:id',
    loadComponent: () => import('./components/curso-detalhes/curso-detalhes').then(m => m.CursoDetalhes)
  },

Bem Legal ne pessoal.

Agora precisamos configurar a parte de quando usuario é inscrito no curso e conseguir ver o conteudo completo ne ? Para isso precisamos implementar parte de login.

serviço de storage.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {
  private readonly TOKEN_KEY = 'access_token';
  private readonly REFRESH_TOKEN_KEY = 'refresh_token';
  private readonly USER_KEY = 'user_data';

  // Token de acesso
  setToken(token: string): void {
    localStorage.setItem(this.TOKEN_KEY, token);
  }

  getToken(): string | null {
    return localStorage.getItem(this.TOKEN_KEY);
  }

  removeToken(): void {
    localStorage.removeItem(this.TOKEN_KEY);
  }

  // Refresh token
  setRefreshToken(token: string): void {
    localStorage.setItem(this.REFRESH_TOKEN_KEY, token);
  }

  getRefreshToken(): string | null {
    return localStorage.getItem(this.REFRESH_TOKEN_KEY);
  }

  removeRefreshToken(): void {
    localStorage.removeItem(this.REFRESH_TOKEN_KEY);
  }

  // Dados do usuário
  setUser(user: any): void {
    localStorage.setItem(this.USER_KEY, JSON.stringify(user));
  }

  getUser(): any {
    const user = localStorage.getItem(this.USER_KEY);
    return user ? JSON.parse(user) : null;
  }

  removeUser(): void {
    localStorage.removeItem(this.USER_KEY);
  }

  // Limpar tudo
  clear(): void {
    this.removeToken();
    this.removeRefreshToken();
    this.removeUser();
  }

  // Verificar se está autenticado
  isAuthenticated(): boolean {
    return !!this.getToken();
  }
}

auth.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { LoginRequest, TokenResponse } from '../models/auth.model';
import { StorageService } from './storage.service';

@Injectable({
  providedIn: 'root' // Torna o serviço acessível em toda a aplicação
})
export class AuthService {
  private apiUrl = 'http://127.0.0.1:8000/api'; // URL base da API Django
  private isAuthenticatedSubject!: BehaviorSubject<boolean>; // Controla o estado de autenticação
	
	
/*	
`BehaviorSubject` é um tipo especial de **Observable** do RxJS que:

1. **Guarda o último valor emitido** (estado atual).
2. **Emite esse valor imediatamente** para qualquer novo assinante.
3. Permite **emitir novos valores manualmente** usando `.next(valor)`.

👉 Exemplo simples:

 ts
import { BehaviorSubject } from 'rxjs';

const authState = new BehaviorSubject<boolean>(false); // valor inicial: false

authState.subscribe(value => console.log('A:', value)); // A: false
authState.next(true); // muda o valor e notifica todos
authState.subscribe(value => console.log('B:', value)); // B: true (recebe o último valor)
*/

	
  isAuthenticated$!: Observable<boolean>; // Observable para acompanhar login/logout

  constructor(
    private http: HttpClient,        // Faz requisições HTTP
    private storage: StorageService  // Gerencia tokens no localStorage
  ) {
    // Inicializa o estado com base no token salvo
    this.isAuthenticatedSubject = new BehaviorSubject<boolean>(this.storage.isAuthenticated());
    this.isAuthenticated$ = this.isAuthenticatedSubject.asObservable();
  }

  // Faz login e salva os tokens
  login(credentials: LoginRequest): Observable<TokenResponse> {
    return this.http.post<TokenResponse>(`${this.apiUrl}/login/`, credentials).pipe(
      tap(response => {
        this.storage.setToken(response.access);
        this.storage.setRefreshToken(response.refresh);
        this.isAuthenticatedSubject.next(true);
      })
    );
  }

  // Faz logout e invalida o token no servidor
  logout() {
    const refreshToken = this.getRefreshToken();

    if (refreshToken) {
      // Envia token de atualização para o backend invalidar
      this.http.post(`${this.apiUrl}/logout/`, { refresh: refreshToken }).subscribe({
        next: () => console.log('Token invalidado no servidor.'),
        error: (err) => console.error('Erro ao invalidar token:', err),
        complete: () => {
          // Limpa storage local e atualiza estado
          this.storage.clear();
          this.isAuthenticatedSubject.next(false);
        }
      });
    } else {
      // Se não há token, apenas limpa tudo
      this.storage.clear();
      this.isAuthenticatedSubject.next(false);
    }
  }

  // Atualiza o token de acesso usando o refresh
  refreshToken(): Observable<{ access: string }> {
    const refreshToken = this.storage.getRefreshToken();
    return this.http.post<{ access: string }>(`${this.apiUrl}/token/refresh/`, {
      refresh: refreshToken
    }).pipe(
      tap(response => this.storage.setToken(response.access))
    );
  }

  // Verifica se o usuário está autenticado
  isAuthenticated(): boolean {
    return this.storage.isAuthenticated();
  }

  // Retorna o token de acesso
  getToken(): string | null {
    return this.storage.getToken();
  }

  // Retorna o refresh token
  getRefreshToken(): string | null {
    return this.storage.getRefreshToken();
  }
}

Tipagem dos dados

models>auth.models.ts

export interface LoginRequest {
  username: string;
  password: string;
}

export interface TokenResponse {
  access: string;
  refresh: string;
  user?: {
    id: number;
    username: string;
    email: string;
    first_name: string;
    last_name: string;
  };
}

export interface User {
  id: number;
  username: string;
  email: string;
  first_name: string;
  last_name: string;
}

interceptor

  • Pega o token salvo no StorageService.
  • Adiciona esse token no header Authorization da requisição (Bearer <token>).
  • Ignora rotas de login e refresh (/token/).
  • Se o servidor responder com erro 401 (token expirado):
    • Tenta renovar o token chamando authService.refreshToken().
    • Se der certo, recarrega a página.
    • Se falhar, faz logout e redireciona pra /login.
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs'; 
import { StorageService } from '../services/storage';
import { AuthService } from '../services/auth';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const storage = inject(StorageService);
  const authService = inject(AuthService);
  const router = inject(Router);

  // Se for rota de login ou refresh token, não adiciona o token
  if (req.url.includes('/token/')) {
    return next(req);
  }

  // Pega o token do storage
  const token = storage.getToken();

  // Se não houver token, não há o que fazer, apenas continue.
  if (!token) {
    return next(req);
  }

  // Clona a requisição e adiciona o cabeçalho de autorização.
  const request = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` },
  });
  // Envia a requisição e trata erros
  return next(request).pipe(
    catchError((error: HttpErrorResponse) => {
      console.error(error);
      // Se o erro for 401 (Não Autorizado), o token pode ter expirado.
      if (error.status === 401) {
        console.error('[Interceptor] Erro 401! Token expirado. Tentando renovar...');

        // Tenta renovar o token
        authService.refreshToken().subscribe({
          next: (response) => {
            console.info('✅ Token renovado com sucesso!');

            // Token foi renovado, recarrega a página para refazer a requisição
            window.location.reload();
          },
          error: (refreshError) => {
            console.error(refreshError);
            console.error('❌ Falha ao renovar token, fazendo logout...');

            // Se falhar ao renovar, desloga
            authService.logout();
            router.navigate(['/login']);
          }
        });
      }

      // Retorna o erro
      return throwError(() => error);
    })
  );
};

config.ts

import { authInterceptor } from './interceptors/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    ...
    provideHttpClient(withInterceptors([authInterceptor])) // Interceptor de autenticação
  ]
};

auth.guard.ts

Verifica se usuario está autenticado ou não, quando acessamos qualquer rota que precisa de autenticação.

import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) { // Se o usuário está autenticado
    return true;
  }

  // Redireciona para login
  router.navigate(['/login']);
  return false;
};

Rota Login

login.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth';
import { StorageService } from '../services/storage';

@Component({
  selector: 'app-login',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './login.html',
  styleUrls: ['./login.scss']
})
export class Login {
  username = '';
  password = '';
  loading = false;
  error = '';

  constructor(
    private authService: AuthService,
    private storage: StorageService,
    private router: Router
  ) {}

  onSubmit(): void {
    if (!this.username || !this.password) {
      this.error = 'Preencha todos os campos';
      return;
    }

    this.loading = true;
    this.error = '';

    let data = {
      username: this.username,
      password: this.password
    };

    this.authService.login(data)
      .subscribe({
        next: (response) => {
          // Salva os dados do usuário no storage
          if (response.user) {
            this.storage.setUser(response.user);
          }
          // Recarrega a página para atualizar o header
          window.location.href = '/cursos';
        },
        error: (err) => {
          this.loading = false;
          this.error = 'Usuário ou senha inválidos';
          console.error('Erro no login:', err);
        }
      });
  }
}
<div class="login-container">
  <div class="login-card">
    <h1>Plataforma EAD</h1>
    <p class="subtitle">Faça login para acessar os cursos</p>

    <form (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label for="username">Usuário</label>
        <input
          type="text"
          id="username"
          [(ngModel)]="username"
          name="username"
          placeholder="Digite seu usuário"
          autocomplete="username"
          [disabled]="loading"
        />
      </div>

      <div class="form-group">
        <label for="password">Senha</label>
        <input
          type="password"
          id="password"
          [(ngModel)]="password"
          name="password"
          placeholder="Digite sua senha"
          autocomplete="current-password"
          [disabled]="loading"
        />
      </div>

      <div class="error" *ngIf="error">{{ error }}</div>

      <button type="submit" [disabled]="loading" class="btn-primary">
        {{ loading ? 'Entrando...' : 'Entrar' }}
      </button>
    </form>

    <div class="info">
      <p><strong>teste:</strong></p>
      <p>Usuário: <code>aluno</code></p>
      <p>Senha: <code>senha123</code></p>
    </div>
  </div>
</div>

scss

.login-container { 
  display: flex;
  align-items: center;
  justify-content: center; 
}

.login-card {
  background: white;
  padding: 40px;
  border-radius: 12px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
  width: 100%;
  max-width: 400px;

  h1 {
    margin: 0 0 10px;
    color: #333;
    text-align: center;
  }

  .subtitle {
    text-align: center;
    color: #666;
    margin-bottom: 30px;
  }
}

.form-group {
  margin-bottom: 20px;

  label {
    display: block;
    margin-bottom: 8px;
    color: #333;
    font-weight: 500;
  }

  input {
    width: 100%;
    padding: 12px;
    border: 1px solid #ddd;
    border-radius: 6px;
    font-size: 14px;
    transition: border-color 0.3s;

    &:focus {
      outline: none;
      border-color: #667eea;
    }

    &:disabled {
      background: #f5f5f5;
      cursor: not-allowed;
    }
  }
}

.error {
  background: #fee;
  color: #c33;
  padding: 12px;
  border-radius: 6px;
  margin-bottom: 20px;
  text-align: center;
  font-size: 14px;
}

.btn-primary {
  width: 100%;
  padding: 14px;
  background: #667eea;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.3s;

  &:hover:not(:disabled) {
    background: #5568d3;
  }

  &:disabled {
    background: #ccc;
    cursor: not-allowed;
  }
}

.info {
  margin-top: 30px;
  padding-top: 20px;
  border-top: 1px solid #eee;
  text-align: center;
  font-size: 13px;
  color: #666;

  code {
    background: #f5f5f5;
    padding: 2px 6px;
    border-radius: 3px;
    color: #667eea;
    font-weight: 600;
  }
}
 

routers

  {
    path: 'login',
    loadComponent: () => import('./login/login').then(m => m.Login)
  },

Vamos criar um header para ficar mais facil

app.ts

user: string = '';

constructor(
  private authService: AuthService,
  private storage: StorageService,
  public router: Router
) {}

ngOnInit(): void {
  const userData = this.storage.getUser();
  this.user = userData?.username || '';
}

logout(): void {
  this.authService.logout(); 
  // Recarrega a página para limpar o estado
  window.location.href = '/cursos';
}

app.html

<header>
    <h1>Cursos Online</h1>
    <div class="right">
        @if (user) {
            <span>Usuário: {{ user }}</span>
            <button (click)="logout()">Sair</button>
        }
        @else {
            <button class="btn-login" (click)="router.navigate(['/login'])">Login</button>
        }
    </div>
</header>

<div class="container">
    <router-outlet></router-outlet>
</div>
header {
  background: #fff;
  padding: 1.2rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

  button {
    padding: 0.6rem 1rem;
    border: none;
    border-radius: 0.4rem;
    cursor: pointer;
    font-weight: 500;
    transition: 0.2s;

    &:first-child {
      background: #f0f0f0;
      &:hover { background: #e0e0e0; }
    }

    &:last-child {
      background: #f44336;
      color: #fff;
      &:hover { background: #d32f2f; }
    }
  }
}  

.right {
    display: flex;
    align-items: center;
    gap: 20px;
}

.btn-login {
    background-color: #667eea;
    color: white;

    &:hover {
        background-color: #5568d3;
    }
}

Perfeito agora que estamos autenticado podemos voltar para detalhes do curso e fazer modificação para mostrar o conteudo do usuario que está inscrito no curso.

curso-detalhes.ts

  isPreview = true;
  isInscrito = false;

	public authService: AuthService

  ifAutenticado(): boolean {
    return this.authService.isAuthenticated();
  }

  loadCurso(id: number): void {
    // Tenta carregar conteúdo completo se estiver logado
    if (this.ifAutenticado()) {
      this.loadConteudo(id)
    } else {
      this.loadPreview(id);
    }
    
   loadConteudo(id: number): void {
	   this.apiService.getCursoConteudo(id).subscribe({
        next: (data) => {
          this.curso = data;
          this.loading = false;
          this.isPreview = false;
          this.isInscrito = true; 
        },
        error: (err) => {
          // Se não inscrito (403), carrega preview
          if (err.status === 403) {
            console.log('Não inscrito - carregando preview');
            this.loadPreview(id);
          } else {
            this.loading = false;
            this.error = 'Erro ao carregar curso';
            console.error('Erro:', err);
          }
        }
      });
    }
    
  }
<!-- INSCRITO -->
<div *ngIf="curso && isInscrito" class="inscrito-layout">

  <div class="main-content">

    <h2>{{ curso.nome }}</h2>

    <div class="stats">
      <span>{{ curso.total_secoes }} seções</span>
      <span>{{ curso.total_aulas }} aulas</span>
    </div>

    @for (secao of curso.secoes; track secao.id) {
    <h4>{{ secao.titulo }}</h4>
    @for (aula of secao.aulas; track aula.id; let idx = $index) {
    <p>Aula {{ idx + 1 }}: {{ aula.titulo }}</p>

      <div class="video-container" *ngIf="aula.video_url">
        <iframe [src]="getYoutubeEmbedUrl(aula.video_url) | safe: 'resourceUrl'" allowfullscreen></iframe>
      </div>

      @if (aula.materiais.length) {
      <div class="materiais">
        <h5>Materiais:</h5>
        @for (material of aula.materiais; track material.id) {
        <p>
          <a [href]="material.arquivo_url" target="_blank">
            {{ material.tipo }}: {{ material.titulo }}
          </a>
        </p>
        }
      </div>
      }

    }
    }

  </div>

  <aside class="sidebar">
    <h3>Conteúdo</h3>
    <div class="secao-menu">

    </div>

  </aside>
</div>

Pegar o Embed do video do youtube

   getYoutubeEmbedUrl(url: string): string {
    const videoId = this.extractYoutubeId(url);
    // console.log(videoId);
    // Extrair o videoId do url
    return videoId ? `https://www.youtube.com/embed/${videoId}` : url;
  }

  private extractYoutubeId(url: string): string | null {
    const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\n?#]+)/;
    const match = url.match(regex);
    // console.log(match);
    return match ? match[1] : null;
  }

pipe

import { Pipe, PipeTransform } from '@angular/core';
import { DomSanitizer, SafeHtml, SafeResourceUrl, SafeScript, SafeStyle, SafeUrl } from '@angular/platform-browser';

@Pipe({
  name: 'safe',
  standalone: true
})
export class SafePipe implements PipeTransform {
  constructor(private sanitizer: DomSanitizer) {}

  transform(value: string, type: string): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl {
    switch (type) {
      case 'html':
        return this.sanitizer.bypassSecurityTrustHtml(value);
      case 'style':
        return this.sanitizer.bypassSecurityTrustStyle(value);
      case 'script':
        return this.sanitizer.bypassSecurityTrustScript(value);
      case 'url':
        return this.sanitizer.bypassSecurityTrustUrl(value);
      case 'resourceUrl':
        return this.sanitizer.bypassSecurityTrustResourceUrl(value);
      default:
        return this.sanitizer.bypassSecurityTrustHtml(value);
    }
  }
}

add no curso-detalhes.scss

// Inscrito específico
.inscrito-layout {
  .video-container {
    position: relative;
    padding-bottom: 56.25%;
    height: 0;
    margin: 1rem 0;
    border-radius: 0.5rem;
    overflow: hidden;
    background: #000;

    iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  }

  .materiais {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid #eee;

    a {
      display: block;
      color: #667eea;
      text-decoration: none;
      padding: 0.3rem 0;

      &:hover {
        text-decoration: underline;
      }
    }
  }

  .secao-menu {
    margin-bottom: 1.5rem;

    h4 {
      font-size: 0.95rem;
      margin-bottom: 0.5rem;
      color: #333;
    }

    a {
      padding: 0.5rem 0.8rem;
      font-size: 0.85rem;
      color: #666;
      cursor: pointer;
      border-radius: 0.3rem;
      transition: 0.2s;
      display: flex;
      justify-content: space-between;

      &:hover {
        background: #f0f0f0;
        color: #667eea;
      }

      &.ativa {
        background: #667eea;
        color: #fff;
        font-weight: 600;
      }

      .duracao-mini {
        font-size: 0.75rem;
        color: #999;
      }
    }
  }
}

Resultado bem legal ne.

Como adicionar as Aulas selecionadas, quando clica vai mudando no menu.

aulaAtiva: any = null;

loadCurso chama this.selecionarPrimeiraAula();

selecionarPrimeiraAula(): void {
    if (this.curso && this.curso.secoes && this.curso.secoes.length > 0) {
      const primeiraSecao = this.curso.secoes[0];
      if (primeiraSecao.aulas && primeiraSecao.aulas.length > 0) {
        this.selecionarAula(primeiraSecao.aulas[0], primeiraSecao.id);
      }
    }
  }

  selecionarAula(aula: any, secaoId: number): void {
    this.aulaAtiva = aula;
  }

  isAulaAtiva(aula: any): boolean {
    return this.aulaAtiva?.id === aula.id;
  }

html atualizado

<!-- INSCRITO -->
<div *ngIf="curso && isInscrito && aulaAtiva" class="inscrito-layout">
  
  <div class="main-content">

    <h2>{{ curso.nome }}</h2>
    <h3>{{ aulaAtiva.titulo }}</h3>  

    <div class="video-container" *ngIf="aulaAtiva.video_url">
      <iframe [src]="getYoutubeEmbedUrl(aulaAtiva.video_url) | safe: 'resourceUrl'" allowfullscreen></iframe>
    </div>
    
    <p *ngIf="aulaAtiva.descricao">{{ aulaAtiva.descricao }}</p>
    
    <div class="materiais" *ngIf="aulaAtiva.materiais?.length">
      <h5>Materiais:</h5>

      @for (material of aulaAtiva.materiais; track material.id) {
        <a [href]="material.arquivo" target="_blank">
          {{ material.tipo }}: {{ material.titulo }}
        </a>
        <a [href]="material.arquivo_url" target="_blank">
          {{ material.titulo }}
        </a>
      }
      
    </div>

  </div>

  <aside class="sidebar">
    <h3>Conteúdo</h3>
    <div class="secao-menu">
      @for (secao of curso.secoes; track secao.id) {
        <h4>{{ secao.titulo }}</h4> 
        @for (aula of secao.aulas; track aula.id; let idx = $index) {
          <a (click)="selecionarAula(aula, secao.id)" [class.ativa]="isAulaAtiva(aula)">
            Aula {{ idx + 1 }}: {{ aula.titulo }}
            <span *ngIf="aula.duracao" class="duracao-mini">{{ aula.duracao }}</span>
          </a>
        }   
      }
    </div>

  </aside>
</div> 

muito bom.

Por fim precisamos fazer parte de inscrição ne. Arrumar os botões.

api

  // Inscrição
  inscreverCurso(id: number): Observable<Inscricao> {
    return this.http.post<Inscricao>(`${this.apiUrl}/cursos/${id}/inscrever/`, {});
  } 

curso-detalhes.ts

 inscrevendo = false;
 
 inscrever(): void {
    const id = this.route.snapshot.paramMap.get('id');
    if (!id) return;

    this.inscrevendo = true;
    this.apiService.inscreverCurso(+id).subscribe({
      next: () => {
        this.inscrevendo = false;
        // Recarrega o curso como inscrito
        this.loadCurso(+id);
      },
      error: (err) => {
        this.inscrevendo = false;
        alert('Erro ao se inscrever no curso');
        console.error('Erro:', err);
      }
    });
  } 

html

  <h3>Inscreva-se agora!</h3>
  <p>Acesse todos os vídeos e materiais</p>

  <button (click)="inscrever()" [disabled]="inscrevendo" *ngIf="ifAutenticado()">
    {{ inscrevendo ? 'Inscrevendo...' : 'Inscrever-se' }}
  </button>
  <button (click)="router.navigate(['/login'])" *ngIf="!ifAutenticado()">
    Fazer Login para Inscrever
  </button>

Minhas Inscrições ou Meus Cursos

minhas-inscricoes>minhas-inscricoes.ts

import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { Inscricao } from '../../models/curso.model';
import { ApiService } from '../../services/api.service';

@Component({
  standalone: true,
  imports: [CommonModule],
  selector: 'app-minhas-inscricoes',
  templateUrl: './minhas-inscricoes.html',
  styleUrls: ['./minhas-inscricoes.scss']
})
export class MinhasInscricoes implements OnInit {

  inscricoes: Inscricao[] = [];

  constructor(
    private apiService: ApiService, 
    private router: Router) { }

  ngOnInit(): void {
    this.apiService.getMinhasInscricoes().subscribe({
      next: (data) => {
        console.log(data);
        this.inscricoes = data;
      },
      error: (err) => {
        console.error('Erro:', err);
      }
    });
  }

  verCurso(id: number): void {
    this.router.navigate(['/curso-detalhes', id]);
  }
}

html

 <h2>Minhas Inscricoes</h2>
@for (inscricao of inscricoes; track inscricao.curso_nome) {

<div class="card-list">
    <h3>{{ inscricao.curso_nome }}</h3>

    <p> 
        {{ (inscricao.data_inscricao | date: 'dd/MM/yyyy') }} às {{ (inscricao.data_inscricao | date: 'HH:mm') }}
    </p> 

    <button (click)="verCurso(inscricao.curso)">Ver curso</button>
</div> 
} 

.card-list {
    display: flex;
    gap: 20px;
    border: 1px solid #ccc;
    padding: 10px;
    margin: 10px;
    flex-direction: column;
    align-items: center;
    flex-wrap: wrap;
}
export interface Inscricao {
  id: number;
  curso: number;
  curso_nome: string;
  usuario: number;
  usuario_nome: string;
  data_inscricao: string;
  ativo: boolean;
}

routers

{
    path: 'minhas-inscricoes',
    loadComponent: () => import('./components/minhas-inscricoes/minhas-inscricoes').then(m => m.MinhasInscricoes),
    canActivate: [authGuard]
}

Que projeto legal ne ? gostaram ?

Comentem ! se inscrevam no canal para mais videos.

Só Ajudando as pessoas. Seja feliz. ❤️

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published