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 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 já 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
.
- Tenta renovar o token chamando
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. ❤️