Spaces:
Running
Running
import { Component, inject, OnInit } from '@angular/core'; | |
import { CommonModule } from '@angular/common'; | |
import { FormsModule } from '@angular/forms'; | |
import { ApiService, Project } from '../../services/api.service'; | |
({ | |
selector: 'app-projects', | |
standalone: true, | |
imports: [CommonModule, FormsModule], | |
template: ` | |
<div class="projects-container"> | |
<div class="toolbar"> | |
<h2>Projects</h2> | |
<div class="toolbar-actions"> | |
<button class="btn btn-primary" (click)="createProject()"> | |
New Project | |
</button> | |
<button class="btn btn-secondary" disabled> | |
Import Project | |
</button> | |
<input | |
type="text" | |
placeholder="Search projects..." | |
[(ngModel)]="searchTerm" | |
(input)="filterProjects()" | |
class="search-input" | |
> | |
<label class="checkbox-label"> | |
<input | |
type="checkbox" | |
[(ngModel)]="showDeleted" | |
(change)="loadProjects()" | |
> | |
Display Deleted | |
</label> | |
<div class="view-toggle"> | |
<button [class.active]="viewMode === 'card'" (click)="viewMode = 'card'"> | |
Card | |
</button> | |
<button [class.active]="viewMode === 'list'" (click)="viewMode = 'list'"> | |
List | |
</button> | |
</div> | |
</div> | |
</div> | |
@if (loading) { | |
<div class="loading"> | |
<span class="spinner"></span> Loading projects... | |
</div> | |
} @else if (filteredProjects.length === 0) { | |
<div class="empty-state"> | |
<p>No projects found.</p> | |
<button class="btn btn-primary" (click)="createProject()"> | |
Create your first project | |
</button> | |
</div> | |
} @else { | |
@if (viewMode === 'card') { | |
<div class="project-cards"> | |
@for (project of filteredProjects; track project.id) { | |
<div class="project-card" [class.disabled]="!project.enabled" [class.deleted]="project.deleted"> | |
<div class="project-icon">π©οΈ</div> | |
<h3>{{ project.name }}</h3> | |
<p>{{ project.caption || 'No description' }}</p> | |
<div class="project-meta"> | |
<span>Versions: {{ project.versions.length || 0 }} ({{ getPublishedCount(project) }} published)</span> | |
<span>Status: {{ project.enabled ? 'β Enabled' : 'β Disabled' }}</span> | |
<span>Last update: {{ getRelativeTime(project.last_update_date) }}</span> | |
</div> | |
<div class="project-actions"> | |
<button class="btn btn-secondary" (click)="editProject(project)">Edit</button> | |
<button class="btn btn-secondary" (click)="manageVersions(project)">Versions</button> | |
<button class="btn btn-secondary" (click)="exportProject(project)">Export</button> | |
<button class="btn btn-secondary" (click)="toggleProject(project)"> | |
{{ project.enabled ? 'Disable' : 'Enable' }} | |
</button> | |
</div> | |
</div> | |
} | |
</div> | |
} @else { | |
<table class="table"> | |
<thead> | |
<tr> | |
<th>Name</th> | |
<th>Caption</th> | |
<th>Versions</th> | |
<th>Enabled</th> | |
<th>Deleted</th> | |
<th>Last Update</th> | |
<th>Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
@for (project of filteredProjects; track project.id) { | |
<tr [class.deleted]="project.deleted"> | |
<td>{{ project.name }}</td> | |
<td>{{ project.caption || '-' }}</td> | |
<td>{{ project.versions.length || 0 }} ({{ getPublishedCount(project) }} published)</td> | |
<td> | |
@if (project.enabled) { | |
<span class="status-badge enabled">β</span> | |
} @else { | |
<span class="status-badge">β</span> | |
} | |
</td> | |
<td> | |
@if (project.deleted) { | |
<span class="status-badge deleted">β</span> | |
} @else { | |
<span class="status-badge">β</span> | |
} | |
</td> | |
<td>{{ getRelativeTime(project.last_update_date) }}</td> | |
<td class="actions"> | |
<button class="action-btn" title="Edit" (click)="editProject(project)">ποΈ</button> | |
<button class="action-btn" title="Versions" (click)="manageVersions(project)">π</button> | |
<button class="action-btn" title="Export" (click)="exportProject(project)">π€</button> | |
@if (!project.deleted) { | |
<button class="action-btn danger" title="Delete" (click)="deleteProject(project)">ποΈ</button> | |
} | |
</td> | |
</tr> | |
} | |
</tbody> | |
</table> | |
} | |
} | |
@if (message) { | |
<div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError"> | |
{{ message }} | |
</div> | |
} | |
</div> | |
`, | |
styles: [` | |
.projects-container { | |
.toolbar { | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
margin-bottom: 1.5rem; | |
h2 { | |
margin: 0; | |
} | |
.toolbar-actions { | |
display: flex; | |
gap: 0.5rem; | |
align-items: center; | |
} | |
} | |
.search-input { | |
padding: 0.375rem 0.75rem; | |
border: 1px solid #ced4da; | |
border-radius: 0.25rem; | |
width: 200px; | |
} | |
.checkbox-label { | |
display: flex; | |
align-items: center; | |
gap: 0.25rem; | |
cursor: pointer; | |
} | |
.view-toggle { | |
display: flex; | |
border: 1px solid #ced4da; | |
border-radius: 0.25rem; | |
overflow: hidden; | |
button { | |
background: white; | |
border: none; | |
padding: 0.375rem 0.75rem; | |
cursor: pointer; | |
&.active { | |
background-color: #007bff; | |
color: white; | |
} | |
} | |
} | |
.loading, .empty-state { | |
text-align: center; | |
padding: 3rem; | |
background-color: white; | |
border-radius: 0.25rem; | |
p { | |
margin-bottom: 1rem; | |
color: #6c757d; | |
} | |
} | |
.project-cards { | |
display: grid; | |
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | |
gap: 1rem; | |
} | |
.project-card { | |
background: white; | |
border: 1px solid #dee2e6; | |
border-radius: 0.5rem; | |
padding: 1.5rem; | |
&:hover { | |
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | |
} | |
&.disabled { | |
opacity: 0.7; | |
} | |
&.deleted { | |
background-color: #f8f9fa; | |
} | |
.project-icon { | |
font-size: 2rem; | |
margin-bottom: 0.5rem; | |
} | |
h3 { | |
margin: 0 0 0.5rem 0; | |
font-size: 1.25rem; | |
} | |
p { | |
color: #6c757d; | |
margin-bottom: 1rem; | |
} | |
.project-meta { | |
font-size: 0.875rem; | |
color: #6c757d; | |
margin-bottom: 1rem; | |
span { | |
display: block; | |
margin-bottom: 0.25rem; | |
} | |
} | |
.project-actions { | |
display: flex; | |
gap: 0.5rem; | |
flex-wrap: wrap; | |
button { | |
flex: 1; | |
min-width: 80px; | |
font-size: 0.875rem; | |
padding: 0.375rem 0.5rem; | |
} | |
} | |
} | |
.status-badge { | |
&.enabled { color: #28a745; } | |
&.deleted { color: #dc3545; } | |
} | |
.actions { | |
display: flex; | |
gap: 0.25rem; | |
} | |
.action-btn { | |
background: none; | |
border: none; | |
cursor: pointer; | |
font-size: 1.1rem; | |
padding: 0.25rem; | |
border-radius: 0.25rem; | |
&:hover { | |
background-color: #f8f9fa; | |
} | |
&.danger:hover { | |
background-color: #f8d7da; | |
} | |
} | |
tr.deleted { | |
opacity: 0.6; | |
background-color: #f8f9fa; | |
} | |
} | |
`] | |
}) | |
export class ProjectsComponent implements OnInit { | |
private apiService = inject(ApiService); | |
projects: Project[] = []; | |
filteredProjects: Project[] = []; | |
loading = true; | |
showDeleted = false; | |
searchTerm = ''; | |
viewMode: 'card' | 'list' = 'card'; | |
message = ''; | |
isError = false; | |
ngOnInit() { | |
this.loadProjects(); | |
} | |
loadProjects() { | |
this.loading = true; | |
this.apiService.getProjects(this.showDeleted).subscribe({ | |
next: (projects) => { | |
this.projects = projects; | |
this.filterProjects(); | |
this.loading = false; | |
}, | |
error: (err) => { | |
this.showMessage('Failed to load projects', true); | |
this.loading = false; | |
} | |
}); | |
} | |
filterProjects() { | |
const term = this.searchTerm.toLowerCase(); | |
this.filteredProjects = this.projects.filter(project => | |
project.name.toLowerCase().includes(term) || | |
(project.caption || '').toLowerCase().includes(term) | |
); | |
} | |
getPublishedCount(project: Project): number { | |
return project.versions.filter(v => v.published).length || 0; | |
} | |
getRelativeTime(timestamp: string): string { | |
const date = new Date(timestamp); | |
const now = new Date(); | |
const diff = now.getTime() - date.getTime(); | |
const minutes = Math.floor(diff / 60000); | |
const hours = Math.floor(diff / 3600000); | |
const days = Math.floor(diff / 86400000); | |
if (minutes < 1) return 'just now'; | |
if (minutes < 60) return `${minutes} min ago`; | |
if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`; | |
return `${days} day${days > 1 ? 's' : ''} ago`; | |
} | |
createProject() { | |
// TODO: Open create dialog | |
console.log('Create project - not implemented yet'); | |
} | |
editProject(project: Project) { | |
// TODO: Open edit dialog | |
console.log('Edit project:', project.name); | |
} | |
manageVersions(project: Project) { | |
// TODO: Open versions dialog | |
console.log('Manage versions:', project.name); | |
} | |
exportProject(project: Project) { | |
this.apiService.exportProject(project.id).subscribe({ | |
next: (data) => { | |
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); | |
const url = window.URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = url; | |
a.download = `${project.name}_export.json`; | |
a.click(); | |
window.URL.revokeObjectURL(url); | |
this.showMessage(`Project "${project.name}" exported successfully`, false); | |
}, | |
error: (err) => { | |
this.showMessage('Failed to export project', true); | |
} | |
}); | |
} | |
toggleProject(project: Project) { | |
this.apiService.toggleProject(project.id).subscribe({ | |
next: (result) => { | |
project.enabled = result.enabled; | |
this.showMessage(`Project "${project.name}" ${result.enabled ? 'enabled' : 'disabled'}`, false); | |
}, | |
error: (err) => { | |
this.showMessage('Failed to toggle project', true); | |
} | |
}); | |
} | |
deleteProject(project: Project) { | |
if (confirm(`Are you sure you want to delete "${project.name}"?`)) { | |
this.apiService.deleteProject(project.id).subscribe({ | |
next: () => { | |
this.showMessage(`Project "${project.name}" deleted successfully`, false); | |
this.loadProjects(); | |
}, | |
error: (err) => { | |
this.showMessage(err.error?.detail || 'Failed to delete project', true); | |
} | |
}); | |
} | |
} | |
private showMessage(message: string, isError: boolean) { | |
this.message = message; | |
this.isError = isError; | |
setTimeout(() => { | |
this.message = ''; | |
}, 5000); | |
} | |
} |