From 2a40f631427ac0bf0d1f0cb8e854215f77ad51e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=9BGabriel=20Wodtke?= Date: Sat, 4 Apr 2026 19:34:30 -0300 Subject: [PATCH 1/2] =?UTF-8?q?=C2=9BFeat:=20CRUD=20de=20areas=20no=20pain?= =?UTF-8?q?el=20do=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/admin/admin.component.ts | 896 +++++++++++++++----- 1 file changed, 693 insertions(+), 203 deletions(-) diff --git a/src/app/components/admin/admin.component.ts b/src/app/components/admin/admin.component.ts index d03a08a..8c6766f 100644 --- a/src/app/components/admin/admin.component.ts +++ b/src/app/components/admin/admin.component.ts @@ -1,13 +1,21 @@ -import { Component, OnInit, OnDestroy, inject, signal, ChangeDetectionStrategy } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { RouterLink } from '@angular/router'; -import { Subscription } from 'rxjs'; -import { CompetencyService } from '../../services/competency.service'; -import { Competency, Resource } from '../../models/competency.model'; - +import { + Component, + OnInit, + OnDestroy, + inject, + signal, + ChangeDetectionStrategy, +} from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule } from "@angular/forms"; +import { RouterLink } from "@angular/router"; +import { Subscription } from "rxjs"; +import { CompetencyService } from "../../services/competency.service"; +import { Competency, Resource } from "../../models/competency.model"; +import { add_new_area } from "../../models/competency.model"; +import { doc } from "firebase/firestore"; @Component({ - selector: 'app-admin', + selector: "app-admin", standalone: true, imports: [CommonModule, FormsModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, @@ -20,14 +28,16 @@ import { Competency, Resource } from '../../models/competency.model';
← Voltar ao Mapa
@if (saved()) { -
✅ Mapa salvo com sucesso! Todos os alunos já verão as mudanças.
+
+ ✅ Mapa salvo com sucesso! Todos os alunos já verão as mudanças. +
} @@ -40,10 +50,113 @@ import { Competency, Resource } from '../../models/competency.model'; class="nav-item" [class.active]="selectedArea()?.id === area.id" [style.border-left-color]="area.color" - (click)="selectArea(area)"> + (click)="selectArea(area)" + > {{ area.icon }} {{ area.name }} } + + + +
+ + +
+ + + @@ -56,7 +169,9 @@ import { Competency, Resource } from '../../models/competency.model';
{{ node.icon }} {{ node.name }} - {{ node.resources.length }} recursos + {{ node.resources.length }} recursos

{{ node.description }}

@@ -71,13 +186,30 @@ import { Competency, Resource } from '../../models/competency.model'; - - - - + + +
} @@ -98,209 +230,456 @@ import { Competency, Resource } from '../../models/competency.model'; `, - styles: [` - .admin { - max-width: 1200px; - margin: 0 auto; - padding: 2rem; - } + styles: [ + ` + .admin { + max-width: 1200px; + margin: 0 auto; + padding: 2rem; + } - .admin-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 1.5rem; - flex-wrap: wrap; - gap: 1rem; - } + .admin-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + } - h1 { font-size: 1.8rem; font-weight: 800; margin: 0 0 0.25rem; color: var(--text-primary); } - .admin-header > div > p { color: var(--text-secondary); margin: 0; } - - .header-actions { display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap; } - - .btn-save { - padding: 0.6rem 1.25rem; - background: var(--gradient); - color: white; - border: none; - border-radius: 8px; - font-weight: 600; - cursor: pointer; - transition: opacity 0.2s; - &:disabled { opacity: 0.6; cursor: not-allowed; } - } + h1 { + font-size: 1.8rem; + font-weight: 800; + margin: 0 0 0.25rem; + color: var(--text-primary); + } + .admin-header > div > p { + color: var(--text-secondary); + margin: 0; + } - .btn-secondary { - padding: 0.6rem 1rem; - border: 1px solid var(--border); - border-radius: 8px; - color: var(--text-secondary); - text-decoration: none; - font-size: 0.875rem; - transition: all 0.2s; - &:hover { border-color: var(--text-primary); color: var(--text-primary); } - } + .header-actions { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; + } - .toast { - padding: 1rem 1.25rem; - border-radius: 10px; - margin-bottom: 1.5rem; - font-weight: 500; - &.success { background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.3); color: #10b981; } - } + .btn-save { + padding: 0.6rem 1.25rem; + background: var(--gradient); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: opacity 0.2s; + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } - .tree-layout { - display: grid; - grid-template-columns: 220px 1fr; - gap: 1.5rem; - min-height: 600px; - } + .btn-secondary { + padding: 0.6rem 1rem; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-secondary); + text-decoration: none; + font-size: 0.875rem; + transition: all 0.2s; + &:hover { + border-color: var(--text-primary); + color: var(--text-primary); + } + } - @media (max-width: 640px) { - .tree-layout { grid-template-columns: 1fr; } - } + .toast { + padding: 1rem 1.25rem; + border-radius: 10px; + margin-bottom: 1.5rem; + font-weight: 500; + &.success { + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.3); + color: #10b981; + } + } - .tree-nav { - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 14px; - padding: 1rem; - height: fit-content; - position: sticky; - top: 80px; - } + .tree-layout { + display: grid; + grid-template-columns: 220px 1fr; + gap: 1.5rem; + min-height: 600px; + } - .nav-title { font-size: 0.7rem; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 0.75rem; padding: 0 0.5rem; } - - .nav-item { - display: block; - width: 100%; - text-align: left; - padding: 0.6rem 0.75rem; - background: transparent; - border: none; - border-left: 3px solid transparent; - border-radius: 0 8px 8px 0; - color: var(--text-secondary); - font-size: 0.875rem; - cursor: pointer; - transition: all 0.15s; - margin-bottom: 0.25rem; - &:hover { background: var(--card-bg); color: var(--text-primary); } - &.active { background: var(--card-bg); color: var(--text-primary); font-weight: 600; } - } + @media (max-width: 640px) { + .tree-layout { + grid-template-columns: 1fr; + } + } - .editor-panel { - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 14px; - padding: 1.5rem; - } + .tree-nav { + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 14px; + padding: 1rem; + height: fit-content; + position: sticky; + top: 80px; + } - h2 { font-size: 1.3rem; font-weight: 700; margin: 0 0 1.5rem; color: var(--text-primary); } + .nav-title { + font-size: 0.7rem; + font-weight: 700; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 0.75rem; + padding: 0 0.5rem; + } - .node-section { - background: var(--card-bg); - border: 1px solid var(--border); - border-radius: 10px; - padding: 1.25rem; - margin-bottom: 1rem; - } + .nav-item { + display: block; + width: 100%; + text-align: left; + padding: 0.6rem 0.75rem; + background: transparent; + border: none; + border-left: 3px solid transparent; + border-radius: 0 8px 8px 0; + color: var(--text-secondary); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.15s; + margin-bottom: 0.25rem; + &:hover { + background: var(--card-bg); + color: var(--text-primary); + } + &.active { + background: var(--card-bg); + color: var(--text-primary); + font-weight: 600; + } + } - .node-title { - display: flex; - justify-content: space-between; - align-items: center; - font-weight: 700; - font-size: 1rem; - color: var(--text-primary); - margin-bottom: 0.5rem; - } + .editor-panel { + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 14px; + padding: 1.5rem; + } - .node-badge { - font-size: 0.7rem; - font-weight: 600; - color: var(--text-muted); - background: var(--panel-bg); - padding: 0.2rem 0.5rem; - border-radius: 4px; - border: 1px solid var(--border); - } + h2 { + font-size: 1.3rem; + font-weight: 700; + margin: 0 0 1.5rem; + color: var(--text-primary); + } - .node-desc { font-size: 0.8rem; color: var(--text-muted); margin: 0 0 1rem; } + .node-section { + background: var(--card-bg); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.25rem; + margin-bottom: 1rem; + } - .res-row { - display: grid; - grid-template-columns: 110px 60px 1fr 1.5fr 32px; - gap: 0.5rem; - align-items: center; - margin-bottom: 0.5rem; - } + .node-title { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 700; + font-size: 1rem; + color: var(--text-primary); + margin-bottom: 0.5rem; + } - @media (max-width: 900px) { - .res-row { grid-template-columns: 1fr 1fr; } - .url-input { grid-column: span 2; } - } + .node-badge { + font-size: 0.7rem; + font-weight: 600; + color: var(--text-muted); + background: var(--panel-bg); + padding: 0.2rem 0.5rem; + border-radius: 4px; + border: 1px solid var(--border); + } - input, select { - padding: 0.45rem 0.6rem; - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-primary); - font-size: 0.8rem; - transition: border-color 0.2s; - &:focus { outline: none; border-color: var(--accent); } - } + .node-desc { + font-size: 0.8rem; + color: var(--text-muted); + margin: 0 0 1rem; + } - .btn-remove { - width: 32px; - height: 32px; - background: transparent; - border: 1px solid var(--border); - border-radius: 6px; - color: var(--text-muted); - cursor: pointer; - font-size: 0.9rem; - transition: all 0.15s; - display: flex; - align-items: center; - justify-content: center; - &:hover { border-color: #ef4444; color: #ef4444; } - } + .res-row { + display: grid; + grid-template-columns: 110px 60px 1fr 1.5fr 32px; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.5rem; + } - .btn-add-resource { - margin-top: 0.5rem; - padding: 0.45rem 1rem; - background: transparent; - border: 1px dashed var(--border); - border-radius: 6px; - color: var(--text-muted); - font-size: 0.8rem; - cursor: pointer; - transition: all 0.2s; - &:hover { border-color: var(--accent); color: var(--accent); } - } + @media (max-width: 900px) { + .res-row { + grid-template-columns: 1fr 1fr; + } + .url-input { + grid-column: span 2; + } + } - .editor-placeholder { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background: var(--panel-bg); - border: 1px solid var(--border); - border-radius: 14px; - gap: 1rem; - color: var(--text-muted); - font-size: 0.95rem; - span { font-size: 3rem; } - } - `] + input, + select { + padding: 0.45rem 0.6rem; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.8rem; + transition: border-color 0.2s; + &:focus { + outline: none; + border-color: var(--accent); + } + } + + .btn-remove { + width: 32px; + height: 32px; + background: transparent; + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-muted); + cursor: pointer; + font-size: 0.9rem; + transition: all 0.15s; + display: flex; + align-items: center; + justify-content: center; + &:hover { + border-color: #ef4444; + color: #ef4444; + } + } + + .btn-add-resource { + margin-top: 0.5rem; + padding: 0.45rem 1rem; + background: transparent; + border: 1px dashed var(--border); + border-radius: 6px; + color: var(--text-muted); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; + &:hover { + border-color: var(--accent); + color: var(--accent); + } + } + + .editor-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background: var(--panel-bg); + border: 1px solid var(--border); + border-radius: 14px; + gap: 1rem; + color: var(--text-muted); + font-size: 0.95rem; + span { + font-size: 3rem; + } + } + + .add-item { + margin-top: 0.5rem; + padding: 0.45rem 1rem; + background: transparent; + border: 1px dashed var(--border); + border-radius: 6px; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; + &:hover { + border-color: var(--accent); + color: var(--accent); + } + } + + .modal_admin { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background-color: rgba(20, 20, 20, 0.55); + justify-content: center; + align-items: center; + } + + #modal_content_admin { + display: flex; + width: 500px; + gap: 15px; + flex-direction: column; + background-color: #0f172a; + padding: 20px; + border-radius: 10px; + } + + .campo { + background-color: #0f172a; + display: flex; + flex-direction: column; + padding: 10px; + border-radius: 8px; + } + + .dropbtn { + background-color: transparent; + color: white; + padding: 16px; + font-size: 16px; + cursor: pointer; + width: 150px; + + border-radius: 10%; + text-align: center; + font-size: 14px; + border-color: #a6b3c5; + } + + .dropbtn:hover, + .dropbtn:focus { + background-color: #297fb93a; + } + + .dropdown { + width: 100%; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .dropdown-content { + display: none; + position: absolute; + background-color: #203147; + min-width: 150px; + border-radius: 10%; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + z-index: 1; + } + + + + .dropdown-content button { + color: var(--text-primary); + padding: 0.6rem 1rem; + text-decoration: none; + display: block; + background: transparent; + border: none; + width: 100%; + text-align: left; + cursor: pointer; + font-size: 0.875rem; + transition: background 0.15s ease; + &:hover { + background: var(--panel-bg); + } + &.active { + background: var(--accent); + color: white; + font-weight: 600; + } + } + + .dropdown-content button:hover { + background-color: #4b4343; + } + + .show { + display: block; + } + + .modal_dropdown { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(20, 20, 20, 0.55); + justify-content: center; + align-items: center; + z-index: 2000; + } + + .modal_dropdown h3 { + color: white; + } + + .modal_content_dropdown h3 { + color: var(--text-primary); + font-size: 1.2rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + letter-spacing: 0.01em; + text-align: left; + } + + .modal_content_dropdown { + background: var(--panel-bg); + border: 1.5px solid var(--border); + border-radius: 14px; + box-shadow: 0 8px 32px 0 rgba(16, 23, 42, 0.45); + padding: 2rem 2.5rem 1.5rem 2.5rem; + min-width: 320px; + min-height: 180px; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 1.5rem; + } + + .btn-remove-area { + padding: 0.75rem 1.25rem; + background: var(--danger); + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + margin-top: 0.5rem; + &:hover { + background: #dc2626; + transform: translateY(-1px); + } + &:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + } + } + `, + ], }) export class AdminComponent implements OnInit, OnDestroy { private competencyService = inject(CompetencyService); + areaName = ""; + areaDescription = ""; + areaColor = "#123456"; + areaIcon = ""; + areas = signal([]); selectedArea = signal(null); saving = signal(false); @@ -311,19 +690,124 @@ export class AdminComponent implements OnInit, OnDestroy { ngOnInit(): void { this.sub.add( - this.competencyService.map$.subscribe(map => { + this.competencyService.map$.subscribe((map) => { this.mapRef = map; this.areas.set(map.children ?? []); // Refresh selected area reference if (this.selectedArea()) { - const updated = (map.children ?? []).find(a => a.id === this.selectedArea()!.id); + const updated = (map.children ?? []).find( + (a) => a.id === this.selectedArea()!.id, + ); if (updated) this.selectedArea.set(updated); } - }) + }), ); + + const dropbtn = document.getElementById("dropbtn"); + const botao_remover = document.getElementById("remover-id-admin"); + const modal_dropdown = document.getElementById("modal_dropdown_remove"); + botao_remover?.addEventListener("click", () => { + if (modal_dropdown !== null) { + modal_dropdown.style.display = "flex"; + } + }); + + const modal = document.getElementById("modal_admin"); + const abrirModal = document.getElementById("abrir-modal-admin"); + const fecharModal = document.getElementById("salvar-modal-admin"); + const conteudo = document.getElementById("modal_content_admin"); + + abrirModal?.addEventListener("click", () => { + if (modal !== null) { + modal.style.display = "flex"; + } + }); + + modal?.addEventListener("click", (event) => { + modal.style.display = "none"; + }); + + conteudo?.addEventListener("click", (event) => { + event.stopPropagation(); + }); + + fecharModal?.addEventListener("click", () => { + if (modal !== null) { + modal?.classList.add("inactive"); + modal.style.display = "none"; + modal?.classList.remove("active"); + } + }); + + const modalDropdown = document.getElementById("modal_dropdown_remove"); + const modalDropdownContent = document.getElementById( + "modal_content_dropdown_remove", + ); + + modalDropdown?.addEventListener("click", (event) => { + if (event.target === modalDropdown) { + modalDropdown.style.display = "none"; + } + }); + modalDropdownContent?.addEventListener("click", (event) => { + event.stopPropagation(); + }); } - ngOnDestroy(): void { this.sub.unsubscribe(); } + dropdown_active() { + const myDropdown = document.getElementById("myDropdown"); + + if (myDropdown !== null) { + myDropdown.classList.toggle("show"); + } + } + + dropdown_inactive() { + const myDropdown = document.getElementById("myDropdown"); + const modalDropdown = document.getElementsByClassName( + "remove-item-dropdown", + ); + + modalDropdown[0].addEventListener("click", () => { + if (myDropdown !== null) { + myDropdown.classList.remove("show"); + } + }); + } + + addArea( + name: string, + description: string, + color: string, + icon: string, + ): void { + if (this.mapRef) { + add_new_area(name, description, color, icon, this.mapRef); + this.areas.set(this.mapRef.children ?? []); + } + } + + selectedAreaToRemove = signal(null); + + removeArea(area: Competency): void { + if (!this.mapRef) return; + this.mapRef.children = (this.mapRef.children ?? []).filter( + (a) => a.id !== area.id, + ); + this.areas.set(this.mapRef.children); + } + + selectAreaToRemove(area: Competency): void { + this.selectedAreaToRemove.set(area); + const myDropdown = document.getElementById("myDropdown"); + if (myDropdown) { + myDropdown.classList.remove("show"); + } + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } selectArea(area: Competency): void { this.selectedArea.set(area); @@ -334,9 +818,15 @@ export class AdminComponent implements OnInit, OnDestroy { } addResource(node: Competency): void { - node.resources = [...node.resources, { - title: '', url: '', type: 'video', language: 'pt' - }]; + node.resources = [ + ...node.resources, + { + title: "", + url: "", + type: "video", + language: "pt", + }, + ]; } removeResource(node: Competency, index: number): void { From 700d5728b7bbced367f442738aab077fb0a58e46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=9BGabriel=20Wodtke?= Date: Mon, 6 Apr 2026 22:09:15 -0300 Subject: [PATCH 2/2] =?UTF-8?q?Corre=C3=A7=C3=A3o=20da=20fun=C3=A7=C3=A3o?= =?UTF-8?q?=20de=20adicionar=20areas=20e=20adiciona=20fun=C3=A7=C3=A3o=20d?= =?UTF-8?q?e=20adicionar=20segmentos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/admin/admin.component.ts | 530 ++++++++++++++------ src/app/models/competency.model.ts | 55 ++ 2 files changed, 427 insertions(+), 158 deletions(-) diff --git a/src/app/components/admin/admin.component.ts b/src/app/components/admin/admin.component.ts index 8c6766f..e52d87f 100644 --- a/src/app/components/admin/admin.component.ts +++ b/src/app/components/admin/admin.component.ts @@ -13,7 +13,7 @@ import { Subscription } from "rxjs"; import { CompetencyService } from "../../services/competency.service"; import { Competency, Resource } from "../../models/competency.model"; import { add_new_area } from "../../models/competency.model"; -import { doc } from "firebase/firestore"; + @Component({ selector: "app-admin", standalone: true, @@ -35,9 +35,9 @@ import { doc } from "firebase/firestore"; @if (saved()) { -
- ✅ Mapa salvo com sucesso! Todos os alunos já verão as mudanças. -
+
+ ✅ Mapa salvo com sucesso! Todos os alunos já verão as mudanças. +
} @@ -46,23 +46,32 @@ import { doc } from "firebase/firestore";