Created project managment page

This commit is contained in:
Marta Borgia Leiva 2026-02-10 11:49:40 +01:00
parent dd043b3385
commit c5b21f166d
3 changed files with 433 additions and 0 deletions

View file

@ -0,0 +1,239 @@
.details-container {
min-height: 100vh;
background-color: #f5f7fa;
}
.content {
padding: 40px;
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
align-items: flex-start;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.btn-back {
padding: 8px 16px;
border-radius: 8px;
border: none;
background-color: #e2e8f0;
color: #1f2933;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-back:hover {
background-color: #cbd5e1;
}
.title h2 {
margin: 0 0 6px 0;
font-size: 26px;
color: #1f2933;
}
.title p {
margin: 0;
color: #6b7280;
}
.project-card,
.collaborators-card,
.tasks-card,
.danger-zone,
.loading,
.error-state,
.empty-state {
background: #ffffff;
padding: 24px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.project-card {
margin-bottom: 20px;
}
.collaborators-card {
margin-bottom: 20px;
}
.danger-zone {
margin-top: 20px;
border: 1px solid #fee2e2;
background: #fff5f5;
}
.danger-header h3 {
margin: 0 0 6px 0;
color: #b91c1c;
}
.danger-header p {
margin: 0;
color: #7f1d1d;
}
.danger-actions {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.danger-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
border-radius: 10px;
background: #ffffff;
border: 1px solid #fecaca;
}
.danger-item h4 {
margin: 0 0 4px 0;
color: #7f1d1d;
font-size: 16px;
}
.danger-item p {
margin: 0;
color: #991b1b;
font-size: 13px;
}
.btn-danger,
.btn-danger-outline {
padding: 8px 14px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.2s ease,
color 0.2s ease,
border-color 0.2s ease;
white-space: nowrap;
}
.btn-danger {
border: none;
background-color: #dc2626;
color: #ffffff;
}
.btn-danger:hover {
background-color: #b91c1c;
}
.btn-danger-outline {
border: 1px solid #fca5a5;
background-color: #ffffff;
color: #b91c1c;
}
.btn-danger-outline:hover {
border-color: #f87171;
background-color: #fee2e2;
}
.project-card h3 {
margin: 0 0 12px 0;
font-size: 22px;
color: #111827;
}
.description {
margin: 0;
color: #4b5563;
}
.tasks-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.collaborators-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.collaborators-count {
font-size: 14px;
color: #6b7280;
}
.collaborators-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.btn-add-task {
width: 100%;
padding: 8px 14px;
border-radius: 8px;
border: none;
background-color: #10b981;
color: #ffffff;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
margin-top: 16px;
}
.btn-add-collaborator {
width: 100%;
padding: 8px 14px;
border-radius: 8px;
border: none;
background-color: #10b981;
color: #ffffff;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
margin-top: 16px;
}
.btn-add-task:hover {
background-color: #059669;
}
.btn-add-collaborator:hover {
background-color: #059669;
}
.tasks-count {
font-size: 14px;
color: #6b7280;
}
.tasks-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.loading p,
.error-state p,
.empty-state p {
margin: 0;
color: #4b5563;
}
.error-state p {
color: #b91c1c;
}

View file

@ -0,0 +1,86 @@
<div class="details-container">
<main class="content">
@if (isLoading()) {
<div class="loading">
<p>Loading project...</p>
</div>
} @else if (errorMessage()) {
<div class="error-state">
<p>{{ errorMessage() }}</p>
</div>
} @else {
<section class="project-card">
<h2>{{ project()?.name }}</h2>
<h4>Project description:</h4>
<p class="description">
{{ project()?.description || 'No description available.' }}
</p>
</section>
<section class="collaborators-card">
<div class="collaborators-header">
<h4>Collaborators</h4>
<span class="collaborators-count"> {{ project()?.users?.length ?? 0 }} total </span>
</div>
@if ((project()?.users?.length ?? 0) > 0) {
<div class="collaborators-grid">
@for (user of project()?.users ?? []; track user.id) {
<app-collaborator-item [user]="user" (remove)="onRemoveCollaborator($event)" />
}
</div>
} @else {
<div class="empty-state">
<p>No collaborators yet.</p>
</div>
}
<button class="btn-add-collaborator" type="button">Add collaborator</button>
</section>
<section class="tasks-card">
<div class="tasks-header">
<h4>Tasks</h4>
<span class="tasks-count">{{ project()?.tasks?.length ?? 0 }} total</span>
</div>
@if ((project()?.tasks?.length ?? 0) > 0) {
<div class="tasks-grid">
@for (task of project()?.tasks ?? []; track task.id) {
<app-task-item [task]="task" />
}
</div>
} @else {
<div class="empty-state">
<p>No tasks yet.</p>
</div>
}
<button class="btn-add-task" type="button" (click)="onAddTask()">Add task</button>
</section>
<section class="danger-zone">
<div class="danger-header">
<h3>Danger zone</h3>
<p>Actions that affect this project permanently.</p>
</div>
<div class="danger-actions">
<div class="danger-item">
<div>
<h4>Edit project details</h4>
<p>Update the project name or description.</p>
</div>
<button class="btn-danger-outline" type="button">Edit project</button>
</div>
<div class="danger-item">
<div>
<h4>Delete project</h4>
<p>This will remove the project and all associated data.</p>
</div>
<button class="btn-danger" type="button">Delete project</button>
</div>
</div>
</section>
}
</main>
</div>

View file

@ -0,0 +1,108 @@
import { Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from '../../services/api.service';
import { ProjectFull } from '../../models/projects.models';
import { CollaboratorItemComponent } from '../../components/collaborator-item/collaborator-item.component';
import { TaskItemComponent } from '../../components/task-item/task-item.component';
import { User } from '../../models/auth.models';
@Component({
selector: 'app-project-details',
standalone: true,
imports: [CommonModule, CollaboratorItemComponent, TaskItemComponent],
templateUrl: './project-details.component.html',
styleUrl: './project-details.component.css',
})
export class ProjectDetailsComponent {
private apiService = inject(ApiService);
private route = inject(ActivatedRoute);
private router = inject(Router);
private projectId: number | null = null;
project = signal<ProjectFull | null>(null);
isLoading = signal(true);
errorMessage = signal('');
constructor() {
const navState = this.router.getCurrentNavigation()?.extras.state ?? (history.state as any);
const initialProject = navState?.project as ProjectFull | undefined;
if (initialProject) {
this.project.set(initialProject);
this.isLoading.set(false);
}
const idParam = this.route.snapshot.paramMap.get('id');
const projectId = idParam ? Number(idParam) : Number.NaN;
if (!Number.isFinite(projectId)) {
this.isLoading.set(false);
this.errorMessage.set('Invalid project id.');
return;
}
this.projectId = projectId;
this.apiService.get<ProjectFull>(`/projects/${projectId}`).subscribe({
next: (project) => {
this.project.set(project);
this.isLoading.set(false);
},
error: (error) => {
this.isLoading.set(false);
if (!initialProject) {
this.errorMessage.set(error?.error?.message || 'Failed to load project details.');
}
},
});
}
onBack() {
this.router.navigate(['/']);
}
onRemoveCollaborator(user: User) {
const current = this.project();
if (!current || this.projectId == null) {
return;
}
const targetId = this.getUserId(user);
if (!targetId) {
return;
}
const previousUsers = current.users ?? [];
const updatedUsers = (current.users ?? []).filter(
(collaborator) => this.getUserId(collaborator) !== targetId,
);
this.project.set({
...current,
users: updatedUsers,
});
this.apiService.delete<void>(`/projects/${this.projectId}/users/${targetId}`).subscribe({
error: (error) => {
this.project.set({
...current,
users: previousUsers,
});
this.errorMessage.set(error?.error?.message || 'Failed to remove collaborator.');
},
});
}
private getUserId(user: User): string {
return String(user.id ?? '');
}
onAddTask() {
if (this.projectId == null) {
return;
}
this.router.navigate(['/projects', this.projectId, 'tasks', 'new']);
}
}