ciyidogan commited on
Commit
b9c256e
·
verified ·
1 Parent(s): aa570a7

Update flare-ui/src/app/components/projects/projects.component.ts

Browse files
flare-ui/src/app/components/projects/projects.component.ts CHANGED
@@ -1,425 +1,558 @@
1
- import { Component, inject, OnInit } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { FormsModule } from '@angular/forms';
4
- import { ApiService, Project } from '../../services/api.service';
5
-
6
- @Component({
7
- selector: 'app-projects',
8
- standalone: true,
9
- imports: [CommonModule, FormsModule],
10
- template: `
11
- <div class="projects-container">
12
- <div class="toolbar">
13
- <h2>Projects</h2>
14
- <div class="toolbar-actions">
15
- <button class="btn btn-primary" (click)="createProject()">
16
- New Project
17
- </button>
18
- <button class="btn btn-secondary" disabled>
19
- Import Project
20
- </button>
21
- <input
22
- type="text"
23
- placeholder="Search projects..."
24
- [(ngModel)]="searchTerm"
25
- (input)="filterProjects()"
26
- class="search-input"
27
- >
28
- <label class="checkbox-label">
29
- <input
30
- type="checkbox"
31
- [(ngModel)]="showDeleted"
32
- (change)="loadProjects()"
33
- >
34
- Display Deleted
35
- </label>
36
- <div class="view-toggle">
37
- <button [class.active]="viewMode === 'card'" (click)="viewMode = 'card'">
38
- Card
39
- </button>
40
- <button [class.active]="viewMode === 'list'" (click)="viewMode = 'list'">
41
- List
42
- </button>
43
- </div>
44
- </div>
45
- </div>
46
-
47
- @if (loading) {
48
- <div class="loading">
49
- <span class="spinner"></span> Loading projects...
50
- </div>
51
- } @else if (filteredProjects.length === 0) {
52
- <div class="empty-state">
53
- <p>No projects found.</p>
54
- <button class="btn btn-primary" (click)="createProject()">
55
- Create your first project
56
- </button>
57
- </div>
58
- } @else {
59
- @if (viewMode === 'card') {
60
- <div class="project-cards">
61
- @for (project of filteredProjects; track project.id) {
62
- <div class="project-card" [class.disabled]="!project.enabled" [class.deleted]="project.deleted">
63
- <div class="project-icon">🛩️</div>
64
- <h3>{{ project.name }}</h3>
65
- <p>{{ project.caption || 'No description' }}</p>
66
- <div class="project-meta">
67
- <span>Versions: {{ project.versions.length || 0 }} ({{ getPublishedCount(project) }} published)</span>
68
- <span>Status: {{ project.enabled ? '✓ Enabled' : '✗ Disabled' }}</span>
69
- <span>Last update: {{ getRelativeTime(project.last_update_date) }}</span>
70
- </div>
71
- <div class="project-actions">
72
- <button class="btn btn-secondary" (click)="editProject(project)">Edit</button>
73
- <button class="btn btn-secondary" (click)="manageVersions(project)">Versions</button>
74
- <button class="btn btn-secondary" (click)="exportProject(project)">Export</button>
75
- <button class="btn btn-secondary" (click)="toggleProject(project)">
76
- {{ project.enabled ? 'Disable' : 'Enable' }}
77
- </button>
78
- </div>
79
- </div>
80
- }
81
- </div>
82
- } @else {
83
- <table class="table">
84
- <thead>
85
- <tr>
86
- <th>Name</th>
87
- <th>Caption</th>
88
- <th>Versions</th>
89
- <th>Enabled</th>
90
- <th>Deleted</th>
91
- <th>Last Update</th>
92
- <th>Actions</th>
93
- </tr>
94
- </thead>
95
- <tbody>
96
- @for (project of filteredProjects; track project.id) {
97
- <tr [class.deleted]="project.deleted">
98
- <td>{{ project.name }}</td>
99
- <td>{{ project.caption || '-' }}</td>
100
- <td>{{ project.versions.length || 0 }} ({{ getPublishedCount(project) }} published)</td>
101
- <td>
102
- @if (project.enabled) {
103
- <span class="status-badge enabled">✓</span>
104
- } @else {
105
- <span class="status-badge">✗</span>
106
- }
107
- </td>
108
- <td>
109
- @if (project.deleted) {
110
- <span class="status-badge deleted">✓</span>
111
- } @else {
112
- <span class="status-badge">✗</span>
113
- }
114
- </td>
115
- <td>{{ getRelativeTime(project.last_update_date) }}</td>
116
- <td class="actions">
117
- <button class="action-btn" title="Edit" (click)="editProject(project)">🖊️</button>
118
- <button class="action-btn" title="Versions" (click)="manageVersions(project)">📋</button>
119
- <button class="action-btn" title="Export" (click)="exportProject(project)">📤</button>
120
- @if (!project.deleted) {
121
- <button class="action-btn danger" title="Delete" (click)="deleteProject(project)">🗑️</button>
122
- }
123
- </td>
124
- </tr>
125
- }
126
- </tbody>
127
- </table>
128
- }
129
- }
130
-
131
- @if (message) {
132
- <div class="alert" [class.alert-success]="!isError" [class.alert-danger]="isError">
133
- {{ message }}
134
- </div>
135
- }
136
- </div>
137
- `,
138
- styles: [`
139
- .projects-container {
140
- .toolbar {
141
- display: flex;
142
- justify-content: space-between;
143
- align-items: center;
144
- margin-bottom: 1.5rem;
145
-
146
- h2 {
147
- margin: 0;
148
- }
149
-
150
- .toolbar-actions {
151
- display: flex;
152
- gap: 0.5rem;
153
- align-items: center;
154
- }
155
- }
156
-
157
- .search-input {
158
- padding: 0.375rem 0.75rem;
159
- border: 1px solid #ced4da;
160
- border-radius: 0.25rem;
161
- width: 200px;
162
- }
163
-
164
- .checkbox-label {
165
- display: flex;
166
- align-items: center;
167
- gap: 0.25rem;
168
- cursor: pointer;
169
- }
170
-
171
- .view-toggle {
172
- display: flex;
173
- border: 1px solid #ced4da;
174
- border-radius: 0.25rem;
175
- overflow: hidden;
176
-
177
- button {
178
- background: white;
179
- border: none;
180
- padding: 0.375rem 0.75rem;
181
- cursor: pointer;
182
-
183
- &.active {
184
- background-color: #007bff;
185
- color: white;
186
- }
187
- }
188
- }
189
-
190
- .loading, .empty-state {
191
- text-align: center;
192
- padding: 3rem;
193
- background-color: white;
194
- border-radius: 0.25rem;
195
-
196
- p {
197
- margin-bottom: 1rem;
198
- color: #6c757d;
199
- }
200
- }
201
-
202
- .project-cards {
203
- display: grid;
204
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
205
- gap: 1rem;
206
- }
207
-
208
- .project-card {
209
- background: white;
210
- border: 1px solid #dee2e6;
211
- border-radius: 0.5rem;
212
- padding: 1.5rem;
213
-
214
- &:hover {
215
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
216
- }
217
-
218
- &.disabled {
219
- opacity: 0.7;
220
- }
221
-
222
- &.deleted {
223
- background-color: #f8f9fa;
224
- }
225
-
226
- .project-icon {
227
- font-size: 2rem;
228
- margin-bottom: 0.5rem;
229
- }
230
-
231
- h3 {
232
- margin: 0 0 0.5rem 0;
233
- font-size: 1.25rem;
234
- }
235
-
236
- p {
237
- color: #6c757d;
238
- margin-bottom: 1rem;
239
- }
240
-
241
- .project-meta {
242
- font-size: 0.875rem;
243
- color: #6c757d;
244
- margin-bottom: 1rem;
245
-
246
- span {
247
- display: block;
248
- margin-bottom: 0.25rem;
249
- }
250
- }
251
-
252
- .project-actions {
253
- display: flex;
254
- gap: 0.5rem;
255
- flex-wrap: wrap;
256
-
257
- button {
258
- flex: 1;
259
- min-width: 80px;
260
- font-size: 0.875rem;
261
- padding: 0.375rem 0.5rem;
262
- }
263
- }
264
- }
265
-
266
- .status-badge {
267
- &.enabled { color: #28a745; }
268
- &.deleted { color: #dc3545; }
269
- }
270
-
271
- .actions {
272
- display: flex;
273
- gap: 0.25rem;
274
- }
275
-
276
- .action-btn {
277
- background: none;
278
- border: none;
279
- cursor: pointer;
280
- font-size: 1.1rem;
281
- padding: 0.25rem;
282
- border-radius: 0.25rem;
283
-
284
- &:hover {
285
- background-color: #f8f9fa;
286
- }
287
-
288
- &.danger:hover {
289
- background-color: #f8d7da;
290
- }
291
- }
292
-
293
- tr.deleted {
294
- opacity: 0.6;
295
- background-color: #f8f9fa;
296
- }
297
- }
298
- `]
299
- })
300
- export class ProjectsComponent implements OnInit {
301
- private apiService = inject(ApiService);
302
-
303
- projects: Project[] = [];
304
- filteredProjects: Project[] = [];
305
- loading = true;
306
- showDeleted = false;
307
- searchTerm = '';
308
- viewMode: 'card' | 'list' = 'card';
309
- message = '';
310
- isError = false;
311
-
312
- ngOnInit() {
313
- this.loadProjects();
314
- }
315
-
316
- loadProjects() {
317
- this.loading = true;
318
- this.apiService.getProjects(this.showDeleted).subscribe({
319
- next: (projects) => {
320
- this.projects = projects;
321
- this.filterProjects();
322
- this.loading = false;
323
- },
324
- error: (err) => {
325
- this.showMessage('Failed to load projects', true);
326
- this.loading = false;
327
- }
328
- });
329
- }
330
-
331
- filterProjects() {
332
- const term = this.searchTerm.toLowerCase();
333
- this.filteredProjects = this.projects.filter(project =>
334
- project.name.toLowerCase().includes(term) ||
335
- (project.caption || '').toLowerCase().includes(term)
336
- );
337
- }
338
-
339
- getPublishedCount(project: Project): number {
340
- return project.versions.filter(v => v.published).length || 0;
341
- }
342
-
343
- getRelativeTime(timestamp: string): string {
344
- const date = new Date(timestamp);
345
- const now = new Date();
346
- const diff = now.getTime() - date.getTime();
347
-
348
- const minutes = Math.floor(diff / 60000);
349
- const hours = Math.floor(diff / 3600000);
350
- const days = Math.floor(diff / 86400000);
351
-
352
- if (minutes < 1) return 'just now';
353
- if (minutes < 60) return `${minutes} min ago`;
354
- if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
355
- return `${days} day${days > 1 ? 's' : ''} ago`;
356
- }
357
-
358
- createProject() {
359
- // TODO: Open create dialog
360
- console.log('Create project - not implemented yet');
361
- }
362
-
363
- editProject(project: Project) {
364
- // TODO: Open edit dialog
365
- console.log('Edit project:', project.name);
366
- }
367
-
368
- manageVersions(project: Project) {
369
- // TODO: Open versions dialog
370
- console.log('Manage versions:', project.name);
371
- }
372
-
373
- exportProject(project: Project) {
374
- this.apiService.exportProject(project.id).subscribe({
375
- next: (data) => {
376
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
377
- const url = window.URL.createObjectURL(blob);
378
- const a = document.createElement('a');
379
- a.href = url;
380
- a.download = `${project.name}_export.json`;
381
- a.click();
382
- window.URL.revokeObjectURL(url);
383
- this.showMessage(`Project "${project.name}" exported successfully`, false);
384
- },
385
- error: (err) => {
386
- this.showMessage('Failed to export project', true);
387
- }
388
- });
389
- }
390
-
391
- toggleProject(project: Project) {
392
- this.apiService.toggleProject(project.id).subscribe({
393
- next: (result) => {
394
- project.enabled = result.enabled;
395
- this.showMessage(`Project "${project.name}" ${result.enabled ? 'enabled' : 'disabled'}`, false);
396
- },
397
- error: (err) => {
398
- this.showMessage('Failed to toggle project', true);
399
- }
400
- });
401
- }
402
-
403
- deleteProject(project: Project) {
404
- if (confirm(`Are you sure you want to delete "${project.name}"?`)) {
405
- this.apiService.deleteProject(project.id).subscribe({
406
- next: () => {
407
- this.showMessage(`Project "${project.name}" deleted successfully`, false);
408
- this.loadProjects();
409
- },
410
- error: (err) => {
411
- this.showMessage(err.error?.detail || 'Failed to delete project', true);
412
- }
413
- });
414
- }
415
- }
416
-
417
- private showMessage(message: string, isError: boolean) {
418
- this.message = message;
419
- this.isError = isError;
420
-
421
- setTimeout(() => {
422
- this.message = '';
423
- }, 5000);
424
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
 
1
+ import { Component, OnInit } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatDialog } from '@angular/material/dialog';
5
+ import { MatSnackBar } from '@angular/material/snack-bar';
6
+ import { MatTableModule } from '@angular/material/table';
7
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import { MatCheckboxModule } from '@angular/material/checkbox';
10
+ import { MatFormFieldModule } from '@angular/material/form-field';
11
+ import { MatInputModule } from '@angular/material/input';
12
+ import { MatButtonToggleModule } from '@angular/material/button-toggle';
13
+ import { MatCardModule } from '@angular/material/card';
14
+ import { MatChipsModule } from '@angular/material/chips';
15
+ import { MatIconModule } from '@angular/material/icon';
16
+ import { ApiService, Project } from '../../services/api.service';
17
+ import { ProjectEditDialogComponent } from '../../dialogs/project-edit-dialog/project-edit-dialog.component';
18
+ import { VersionEditDialogComponent } from '../../dialogs/version-edit-dialog/version-edit-dialog.component';
19
+ import { ConfirmDialogComponent } from '../../dialogs/confirm-dialog/confirm-dialog.component';
20
+
21
+ @Component({
22
+ selector: 'app-projects',
23
+ standalone: true,
24
+ imports: [
25
+ CommonModule,
26
+ FormsModule,
27
+ MatTableModule,
28
+ MatProgressBarModule,
29
+ MatButtonModule,
30
+ MatCheckboxModule,
31
+ MatFormFieldModule,
32
+ MatInputModule,
33
+ MatButtonToggleModule,
34
+ MatCardModule,
35
+ MatChipsModule,
36
+ MatIconModule
37
+ ],
38
+ templateUrl: './projects.component.html',
39
+ styleUrls: ['./projects.component.scss']
40
+ })
41
+ export class ProjectsComponent implements OnInit {
42
+ projects: Project[] = [];
43
+ filteredProjects: Project[] = [];
44
+ searchTerm = '';
45
+ showDeleted = false;
46
+ viewMode: 'list' | 'card' = 'card';
47
+ loading = false;
48
+ message = '';
49
+ isError = false;
50
+
51
+ constructor(
52
+ private apiService: ApiService,
53
+ private dialog: MatDialog,
54
+ private snackBar: MatSnackBar
55
+ ) {}
56
+
57
+ ngOnInit() {
58
+ this.loadProjects();
59
+ }
60
+
61
+ async loadProjects() {
62
+ this.loading = true;
63
+ try {
64
+ this.projects = await this.apiService.getProjects(this.showDeleted).toPromise() || [];
65
+ this.applyFilter();
66
+ } catch (error) {
67
+ this.showMessage('Failed to load projects', true);
68
+ } finally {
69
+ this.loading = false;
70
+ }
71
+ }
72
+
73
+ applyFilter() {
74
+ this.filteredProjects = this.projects.filter(project => {
75
+ const matchesSearch = !this.searchTerm ||
76
+ project.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
77
+ (project.caption || '').toLowerCase().includes(this.searchTerm.toLowerCase());
78
+
79
+ const matchesDeleted = this.showDeleted || !project.deleted;
80
+
81
+ return matchesSearch && matchesDeleted;
82
+ });
83
+ }
84
+
85
+ filterProjects() {
86
+ this.applyFilter();
87
+ }
88
+
89
+ onSearchChange() {
90
+ this.applyFilter();
91
+ }
92
+
93
+ onShowDeletedChange() {
94
+ this.loadProjects();
95
+ }
96
+
97
+ createProject() {
98
+ const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
99
+ width: '500px',
100
+ data: { mode: 'create' }
101
+ });
102
+
103
+ dialogRef.afterClosed().subscribe(result => {
104
+ if (result) {
105
+ this.loadProjects();
106
+ }
107
+ });
108
+ }
109
+
110
+ editProject(project: Project) {
111
+ const dialogRef = this.dialog.open(ProjectEditDialogComponent, {
112
+ width: '500px',
113
+ data: { mode: 'edit', project: { ...project } }
114
+ });
115
+
116
+ dialogRef.afterClosed().subscribe(result => {
117
+ if (result) {
118
+ this.loadProjects();
119
+ }
120
+ });
121
+ }
122
+
123
+ async toggleProject(project: Project) {
124
+ try {
125
+ const result = await this.apiService.toggleProject(project.id).toPromise();
126
+ project.enabled = result.enabled;
127
+ this.showMessage(
128
+ `Project "${project.name}" ${project.enabled ? 'enabled' : 'disabled'} successfully`,
129
+ false
130
+ );
131
+ } catch (error) {
132
+ this.showMessage('Failed to toggle project', true);
133
+ }
134
+ }
135
+
136
+ manageVersions(project: Project) {
137
+ const dialogRef = this.dialog.open(VersionEditDialogComponent, {
138
+ width: '90vw',
139
+ maxWidth: '1200px',
140
+ height: '90vh',
141
+ data: { project }
142
+ });
143
+
144
+ dialogRef.afterClosed().subscribe(result => {
145
+ if (result) {
146
+ this.loadProjects();
147
+ }
148
+ });
149
+ }
150
+
151
+ async deleteProject(project: Project) {
152
+ const hasVersions = project.versions && project.versions.length > 0;
153
+ const message = hasVersions ?
154
+ `Project "${project.name}" has ${project.versions.length} version(s). Are you sure you want to delete it?` :
155
+ `Are you sure you want to delete project "${project.name}"?`;
156
+
157
+ const dialogRef = this.dialog.open(ConfirmDialogComponent, {
158
+ width: '400px',
159
+ data: {
160
+ title: 'Delete Project',
161
+ message: message,
162
+ confirmText: 'Delete',
163
+ confirmColor: 'warn'
164
+ }
165
+ });
166
+
167
+ dialogRef.afterClosed().subscribe(async confirmed => {
168
+ if (confirmed) {
169
+ try {
170
+ await this.apiService.deleteProject(project.id).toPromise();
171
+ this.showMessage('Project deleted successfully', false);
172
+ this.loadProjects();
173
+ } catch (error: any) {
174
+ const message = error.error?.detail || 'Failed to delete project';
175
+ this.showMessage(message, true);
176
+ }
177
+ }
178
+ });
179
+ }
180
+
181
+ async exportProject(project: Project) {
182
+ try {
183
+ const data = await this.apiService.exportProject(project.id).toPromise();
184
+
185
+ // Create and download file
186
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
187
+ const url = window.URL.createObjectURL(blob);
188
+ const link = document.createElement('a');
189
+ link.href = url;
190
+ link.download = `${project.name}_export_${new Date().getTime()}.json`;
191
+ link.click();
192
+ window.URL.revokeObjectURL(url);
193
+
194
+ this.showMessage('Project exported successfully', false);
195
+ } catch (error) {
196
+ this.showMessage('Failed to export project', true);
197
+ }
198
+ }
199
+
200
+ importProject() {
201
+ const input = document.createElement('input');
202
+ input.type = 'file';
203
+ input.accept = '.json';
204
+
205
+ input.onchange = async (event: any) => {
206
+ const file = event.target.files[0];
207
+ if (!file) return;
208
+
209
+ try {
210
+ const text = await file.text();
211
+ const data = JSON.parse(text);
212
+
213
+ await this.apiService.importProject(data).toPromise();
214
+ this.showMessage('Project imported successfully', false);
215
+ this.loadProjects();
216
+ } catch (error: any) {
217
+ const message = error.error?.detail || 'Failed to import project';
218
+ this.showMessage(message, true);
219
+ }
220
+ };
221
+
222
+ input.click();
223
+ }
224
+
225
+ getPublishedCount(project: Project): number {
226
+ return project.versions?.filter(v => v.published).length || 0;
227
+ }
228
+
229
+ getRelativeTime(timestamp: string | undefined): string {
230
+ if (!timestamp) return 'Never';
231
+
232
+ const date = new Date(timestamp);
233
+ const now = new Date();
234
+ const diffMs = now.getTime() - date.getTime();
235
+ const diffMins = Math.floor(diffMs / 60000);
236
+ const diffHours = Math.floor(diffMs / 3600000);
237
+ const diffDays = Math.floor(diffMs / 86400000);
238
+
239
+ if (diffMins < 60) return `${diffMins} minutes ago`;
240
+ if (diffHours < 24) return `${diffHours} hours ago`;
241
+ if (diffDays < 7) return `${diffDays} days ago`;
242
+
243
+ return date.toLocaleDateString();
244
+ }
245
+
246
+ private showMessage(message: string, isError: boolean) {
247
+ this.message = message;
248
+ this.isError = isError;
249
+
250
+ setTimeout(() => {
251
+ this.message = '';
252
+ }, 5000);
253
+ }
254
+ }
255
+
256
+
257
+
258
+
259
+ import { Component, inject, OnInit } from '@angular/core';
260
+ import { CommonModule } from '@angular/common';
261
+ import { FormsModule } from '@angular/forms';
262
+ import { ApiService, Project } from '../../services/api.service';
263
+
264
+ @Component({
265
+ selector: 'app-projects',
266
+ standalone: true,
267
+ imports: [CommonModule, FormsModule],
268
+ template: `
269
+
270
+ `,
271
+ styles: [`
272
+ .projects-container {
273
+ .toolbar {
274
+ display: flex;
275
+ justify-content: space-between;
276
+ align-items: center;
277
+ margin-bottom: 1.5rem;
278
+
279
+ h2 {
280
+ margin: 0;
281
+ }
282
+
283
+ .toolbar-actions {
284
+ display: flex;
285
+ gap: 0.5rem;
286
+ align-items: center;
287
+ }
288
+ }
289
+
290
+ .search-input {
291
+ padding: 0.375rem 0.75rem;
292
+ border: 1px solid #ced4da;
293
+ border-radius: 0.25rem;
294
+ width: 200px;
295
+ }
296
+
297
+ .checkbox-label {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: 0.25rem;
301
+ cursor: pointer;
302
+ }
303
+
304
+ .view-toggle {
305
+ display: flex;
306
+ border: 1px solid #ced4da;
307
+ border-radius: 0.25rem;
308
+ overflow: hidden;
309
+
310
+ button {
311
+ background: white;
312
+ border: none;
313
+ padding: 0.375rem 0.75rem;
314
+ cursor: pointer;
315
+
316
+ &.active {
317
+ background-color: #007bff;
318
+ color: white;
319
+ }
320
+ }
321
+ }
322
+
323
+ .loading, .empty-state {
324
+ text-align: center;
325
+ padding: 3rem;
326
+ background-color: white;
327
+ border-radius: 0.25rem;
328
+
329
+ p {
330
+ margin-bottom: 1rem;
331
+ color: #6c757d;
332
+ }
333
+ }
334
+
335
+ .project-cards {
336
+ display: grid;
337
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
338
+ gap: 1rem;
339
+ }
340
+
341
+ .project-card {
342
+ background: white;
343
+ border: 1px solid #dee2e6;
344
+ border-radius: 0.5rem;
345
+ padding: 1.5rem;
346
+
347
+ &:hover {
348
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
349
+ }
350
+
351
+ &.disabled {
352
+ opacity: 0.7;
353
+ }
354
+
355
+ &.deleted {
356
+ background-color: #f8f9fa;
357
+ }
358
+
359
+ .project-icon {
360
+ font-size: 2rem;
361
+ margin-bottom: 0.5rem;
362
+ }
363
+
364
+ h3 {
365
+ margin: 0 0 0.5rem 0;
366
+ font-size: 1.25rem;
367
+ }
368
+
369
+ p {
370
+ color: #6c757d;
371
+ margin-bottom: 1rem;
372
+ }
373
+
374
+ .project-meta {
375
+ font-size: 0.875rem;
376
+ color: #6c757d;
377
+ margin-bottom: 1rem;
378
+
379
+ span {
380
+ display: block;
381
+ margin-bottom: 0.25rem;
382
+ }
383
+ }
384
+
385
+ .project-actions {
386
+ display: flex;
387
+ gap: 0.5rem;
388
+ flex-wrap: wrap;
389
+
390
+ button {
391
+ flex: 1;
392
+ min-width: 80px;
393
+ font-size: 0.875rem;
394
+ padding: 0.375rem 0.5rem;
395
+ }
396
+ }
397
+ }
398
+
399
+ .status-badge {
400
+ &.enabled { color: #28a745; }
401
+ &.deleted { color: #dc3545; }
402
+ }
403
+
404
+ .actions {
405
+ display: flex;
406
+ gap: 0.25rem;
407
+ }
408
+
409
+ .action-btn {
410
+ background: none;
411
+ border: none;
412
+ cursor: pointer;
413
+ font-size: 1.1rem;
414
+ padding: 0.25rem;
415
+ border-radius: 0.25rem;
416
+
417
+ &:hover {
418
+ background-color: #f8f9fa;
419
+ }
420
+
421
+ &.danger:hover {
422
+ background-color: #f8d7da;
423
+ }
424
+ }
425
+
426
+ tr.deleted {
427
+ opacity: 0.6;
428
+ background-color: #f8f9fa;
429
+ }
430
+ }
431
+ `]
432
+ })
433
+ export class ProjectsComponent implements OnInit {
434
+ private apiService = inject(ApiService);
435
+
436
+ projects: Project[] = [];
437
+ filteredProjects: Project[] = [];
438
+ loading = true;
439
+ showDeleted = false;
440
+ searchTerm = '';
441
+ viewMode: 'card' | 'list' = 'card';
442
+ message = '';
443
+ isError = false;
444
+
445
+ ngOnInit() {
446
+ this.loadProjects();
447
+ }
448
+
449
+ loadProjects() {
450
+ this.loading = true;
451
+ this.apiService.getProjects(this.showDeleted).subscribe({
452
+ next: (projects) => {
453
+ this.projects = projects;
454
+ this.filterProjects();
455
+ this.loading = false;
456
+ },
457
+ error: (err) => {
458
+ this.showMessage('Failed to load projects', true);
459
+ this.loading = false;
460
+ }
461
+ });
462
+ }
463
+
464
+ filterProjects() {
465
+ const term = this.searchTerm.toLowerCase();
466
+ this.filteredProjects = this.projects.filter(project =>
467
+ project.name.toLowerCase().includes(term) ||
468
+ (project.caption || '').toLowerCase().includes(term)
469
+ );
470
+ }
471
+
472
+ getPublishedCount(project: Project): number {
473
+ return project.versions.filter(v => v.published).length || 0;
474
+ }
475
+
476
+ getRelativeTime(timestamp: string): string {
477
+ const date = new Date(timestamp);
478
+ const now = new Date();
479
+ const diff = now.getTime() - date.getTime();
480
+
481
+ const minutes = Math.floor(diff / 60000);
482
+ const hours = Math.floor(diff / 3600000);
483
+ const days = Math.floor(diff / 86400000);
484
+
485
+ if (minutes < 1) return 'just now';
486
+ if (minutes < 60) return `${minutes} min ago`;
487
+ if (hours < 24) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
488
+ return `${days} day${days > 1 ? 's' : ''} ago`;
489
+ }
490
+
491
+ createProject() {
492
+ // TODO: Open create dialog
493
+ console.log('Create project - not implemented yet');
494
+ }
495
+
496
+ editProject(project: Project) {
497
+ // TODO: Open edit dialog
498
+ console.log('Edit project:', project.name);
499
+ }
500
+
501
+ manageVersions(project: Project) {
502
+ // TODO: Open versions dialog
503
+ console.log('Manage versions:', project.name);
504
+ }
505
+
506
+ exportProject(project: Project) {
507
+ this.apiService.exportProject(project.id).subscribe({
508
+ next: (data) => {
509
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
510
+ const url = window.URL.createObjectURL(blob);
511
+ const a = document.createElement('a');
512
+ a.href = url;
513
+ a.download = `${project.name}_export.json`;
514
+ a.click();
515
+ window.URL.revokeObjectURL(url);
516
+ this.showMessage(`Project "${project.name}" exported successfully`, false);
517
+ },
518
+ error: (err) => {
519
+ this.showMessage('Failed to export project', true);
520
+ }
521
+ });
522
+ }
523
+
524
+ toggleProject(project: Project) {
525
+ this.apiService.toggleProject(project.id).subscribe({
526
+ next: (result) => {
527
+ project.enabled = result.enabled;
528
+ this.showMessage(`Project "${project.name}" ${result.enabled ? 'enabled' : 'disabled'}`, false);
529
+ },
530
+ error: (err) => {
531
+ this.showMessage('Failed to toggle project', true);
532
+ }
533
+ });
534
+ }
535
+
536
+ deleteProject(project: Project) {
537
+ if (confirm(`Are you sure you want to delete "${project.name}"?`)) {
538
+ this.apiService.deleteProject(project.id).subscribe({
539
+ next: () => {
540
+ this.showMessage(`Project "${project.name}" deleted successfully`, false);
541
+ this.loadProjects();
542
+ },
543
+ error: (err) => {
544
+ this.showMessage(err.error?.detail || 'Failed to delete project', true);
545
+ }
546
+ });
547
+ }
548
+ }
549
+
550
+ private showMessage(message: string, isError: boolean) {
551
+ this.message = message;
552
+ this.isError = isError;
553
+
554
+ setTimeout(() => {
555
+ this.message = '';
556
+ }, 5000);
557
+ }
558
  }