diff --git a/src/app/components/admin/admin.component.ts b/src/app/components/admin/admin.component.ts index d03a08a..e52d87f 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"; @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. +
} @@ -36,271 +46,766 @@ import { Competency, Resource } from '../../models/competency.model'; @if (selectedArea()) { -
-

{{ selectedArea()!.icon }} {{ selectedArea()!.name }}

- - - @for (node of getNodesForArea(selectedArea()!); track node.id) { -
-
- {{ node.icon }} {{ node.name }} - {{ node.resources.length }} recursos -
+
+

{{ selectedArea()!.icon }} {{ selectedArea()!.name }}

-

{{ node.description }}

- - -
- @for (res of node.resources; track $index; let i = $index) { -
- - - - - -
- } + + @for (node of getNodesForArea(selectedArea()!); track node.id) { +
+
+ {{ node.icon }} {{ node.name }} + {{ node.resources.length }} recursos +
- - +

{{ node.description }}

+ + +
+ @for (res of node.resources; track $index; let i = $index) { +
+ + + + + +
+ } + + + +
+ +
+ } + +
+ + +
+ + + + + + + +
} @else { -
- 👈 -

Selecione uma área para editar seus recursos

-
+
+ 👈 +

Selecione uma área para editar seus recursos

+
}
`, - 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: flex; + 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, + #modal_content_segment { + 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: flex; + 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 +816,212 @@ 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); } }) ); } - ngOnDestroy(): void { this.sub.unsubscribe(); } + isAddAreaModalVisible = false; + isRemoveAreaModalVisible = false; + isDropdownOpen = false; + + openAddAreaModal() { + this.isAddAreaModalVisible = true; + } + closeAddAreaModal() { + this.isAddAreaModalVisible = false; + } + + openRemoveAreaModal() { + this.isRemoveAreaModalVisible = true; + } + closeRemoveAreaModal() { + this.isRemoveAreaModalVisible = false; + } + + isAddSegmentModalVisible = false; + isRemoveSegmentModalVisible = false; + isSegmentDropdownOpen = false; + segmentName = ""; + segmentDescription = ""; + segmentIcon = ""; + selectedNodeForSegment: Competency | null = null; + selectedSegmentToRemove = signal(null); + + openAddSegmentModal(node: Competency) { + this.selectedNodeForSegment = node; + this.segmentIcon = node.icon; + this.isAddSegmentModalVisible = true; + } + + closeAddSegmentModal() { + this.isAddSegmentModalVisible = false; + this.selectedNodeForSegment = null; + this.segmentName = ""; + this.segmentDescription = ""; + this.segmentIcon = ""; + } + + openRemoveSegmentModal() { + this.isRemoveSegmentModalVisible = true; + this.selectedSegmentToRemove.set(null); + this.isSegmentDropdownOpen = false; + } + + closeRemoveSegmentModal() { + this.isRemoveSegmentModalVisible = false; + this.selectedSegmentToRemove.set(null); + this.isSegmentDropdownOpen = false; + } + + toggleSegmentDropdown() { + this.isSegmentDropdownOpen = !this.isSegmentDropdownOpen; + } + + selectSegmentToRemove(segment: Competency) { + this.selectedSegmentToRemove.set(segment); + this.isSegmentDropdownOpen = false; + } + + removeSegment(segment: Competency) { + if (!this.mapRef || !segment) return; + + // Alerta: Não permitimos remover o root ou a área selecionada por este botão + if (segment.id === 'root' || (this.selectedArea() && segment.id === this.selectedArea()?.id)) { + this.closeRemoveSegmentModal(); + return; + } + + // Função recursiva para encontrar o pai e remover o filho usando splice + const removeRecursive = (root: Competency, target: Competency): boolean => { + if (!root.children) return false; + + const index = root.children.findIndex(c => + c === target || (target.id && c.id === target.id) + ); + + if (index !== -1) { + root.children.splice(index, 1); + return true; + } + + for (const child of root.children) { + if (removeRecursive(child, target)) return true; + } + return false; + }; + + if (removeRecursive(this.mapRef, segment)) { + this.areas.set([...(this.mapRef.children ?? [])]); + + if (this.selectedArea()) { + const updatedArea = this.findNodeInMap(this.mapRef, this.selectedArea()!.id); + if (updatedArea) { + this.selectedArea.set({ ...updatedArea }); + } else { + this.selectedArea.set(null); + } + } + } + + this.closeRemoveSegmentModal(); + } + + private findNodeInMap(root: Competency, id: string): Competency | null { + if (root.id === id) return root; + if (root.children) { + for (const child of root.children) { + const found = this.findNodeInMap(child, id); + if (found) return found; + } + } + return null; + } + + addSegment(): void { + const areaRef = this.selectedArea(); + if (!this.segmentName?.trim() || !this.mapRef || !areaRef) { + return; + } + + const targetNode = this.findNodeInMap(this.mapRef, areaRef.id); + if (targetNode) { + if (!targetNode.children) targetNode.children = []; + + targetNode.children.push({ + id: "seg_" + Date.now(), + name: this.segmentName, + description: this.segmentDescription, + color: targetNode.color, + icon: this.segmentIcon || targetNode.icon, + resources: [], + children: [], + }); + + this.selectedArea.set({ ...targetNode }); + this.areas.set([...(this.mapRef.children ?? [])]); + } + + this.closeAddSegmentModal(); + } + + dropdown_active() { + this.isDropdownOpen = !this.isDropdownOpen; + } + + closeDropdown() { + this.isDropdownOpen = false; + } + + addArea( + name: string, + description: string, + color: string, + icon: string + ): void { + if (!name?.trim()) { + return; + } + if (this.mapRef) { + add_new_area(name, description, color, icon, this.mapRef); + this.areas.set(this.mapRef.children ?? []); + this.areaName = ""; + this.areaDescription = ""; + this.areaColor = "#123456"; + this.areaIcon = ""; + this.closeAddAreaModal(); + } + + } + + 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); + this.closeRemoveAreaModal(); + } + + selectAreaToRemove(area: Competency): void { + this.selectedAreaToRemove.set(area); + this.closeDropdown(); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } selectArea(area: Competency): void { this.selectedArea.set(area); @@ -334,9 +1032,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 { diff --git a/src/app/models/competency.model.ts b/src/app/models/competency.model.ts index d2b93f8..d5a6749 100644 --- a/src/app/models/competency.model.ts +++ b/src/app/models/competency.model.ts @@ -209,3 +209,58 @@ export const DEFAULT_COMPETENCY_MAP: Competency = { } ] }; + +export function add_new_area( + name: string, + description: string, + color: string, + icon: string, + node: Competency +): void { + if (!node.children) node.children = []; + const newCompetency: Competency = { + id: 'area_' + Date.now(), + name, + description, + color, + icon, + resources: [], + children: [] + }; + node.children.push(newCompetency); +} + +export function remove_area(competencyId: string, node: Competency): boolean { + if (!node.children) return false; + const index = node.children.findIndex(c => c.id === competencyId); + if (index !== -1) { + node.children.splice(index, 1); + return true; + } + else{ + return false; + } +} + +export function add_new_Resource( + title: string, + url: string, + type: 'video' | 'article' | 'course' | 'docs', + language: 'pt' | 'en', + node: Competency +): void { + if (!node.resources) node.resources = []; + const newResource: Resource = { + title, + url, + type, + language + }; + node.resources.push(newResource); +} + +export function remove_resource(resourceId: string, nodeId: string): void { + if (!DEFAULT_COMPETENCY_MAP.children) return ; + const node = DEFAULT_COMPETENCY_MAP.children.find(child => child.id === nodeId); + node?.children?.filter(child => child.id === nodeId); +}