Refactor tutorial management and add AI image model functionality

- Updated application.properties for local database configuration.
- Introduced DateTimeHelper for timestamp parsing.
- Added MyAIImageModel and MyImageOptions for AI image processing.
- Enhanced Tutorial model to include created and modified timestamps.
- Implemented tutorial editing component with improved UI and functionality.
- Updated user API service to handle tutorial retrieval and updates.
- Refactored tutorials list component to support selection and batch deletion.
This commit is contained in:
liosha84
2025-07-12 18:30:09 +03:00
parent dbdc189c12
commit f9306f7af8
19 changed files with 930 additions and 101 deletions
@@ -1,6 +1,47 @@
export class Tutorial {
isSelected?: boolean;
id?: any;
title?: string;
description?: string;
published?: boolean;
created?: Date;
modified?: Date;
isEdit?: boolean;
}
export const TutorialColumns = [
{
key: 'isSelected',
type: 'isSelected',
label: '',
},
{
key: 'title',
type: 'text',
label: 'Title',
required: true,
},
{
key: 'published',
type: 'boolean',
label: 'Is Published',
},
{
key: 'created',
type: 'datetime',
label: 'Created Date',
required: true,
},
{
key: 'modified',
type: 'datetime',
label: 'Modified Date',
required: true
},
{
key: 'isEdit',
type: 'isEdit',
label: '',
}
];
@@ -0,0 +1,41 @@
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title>Edit tutorial</mat-card-title>
</mat-card-header>
<mat-card-content >
<mat-form-field class="example-full-width">
<mat-label>Title</mat-label>
<input matInput required
[(ngModel)]="tutorial.title">
</mat-form-field>
<mat-tab-group>
<mat-tab label="Markdown text">
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
<markdown class="variable-binding" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Result">
<markdown class="preview" [data]="tutorial.description"></markdown>
</mat-tab>
<mat-tab label="Example">
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
<markdown class="variable-binding" [data]="markdown"></markdown>
</mat-tab>
</mat-tab-group>
</mat-card-content>
<mat-card-actions>
<button matButton (click)="updateTutorial()">Save</button>
@if (hasError){
<mat-error>
{{errorMessage}}
</mat-error>
}
@if(submitted) {
<h4>Tutorial was submitted successfully!</h4>
<button matButton routerLink="../tutorial-add">Add new tutorial</button>
<button matButton routerLink="../tutorials-list">Back to list</button>
}
</mat-card-actions>
</mat-card>
@@ -0,0 +1,52 @@
.submit-form {
max-width: 400px;
margin: auto;
}
mat-card{
margin: 20px;
}
mat-card-title{
color: cyan;
}
.example-form {
min-width: 150px;
max-width: 500px;
width: 100%;
}
.example-full-width {
width: 100%;
}
.variable-binding,
.variable-textarea {
width: 49%;
}
.variable-textarea {
border-radius: 5px;
box-shadow: 0 1px 2px rgba(0,0,0,.07);
min-height: 420px;
padding: 8px;
transition: all 300ms ease-out;
}
.variable-textarea:hover {
box-shadow: 0 6px 12px 3px rgba(0,0,0,.09),
0 2px 3px 1px rgba(0,0,0,.06);
}
.variable-binding {
display: block;
float: right;
}
.preview {
/* display: block;
float: right;*/
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TutorialEditComponent } from './tutorial-edit.component';
describe('TutorialEditComponent', () => {
let component: TutorialEditComponent;
let fixture: ComponentFixture<TutorialEditComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TutorialEditComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TutorialEditComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,91 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MarkdownComponent} from 'ngx-markdown';
import {MatButton} from '@angular/material/button';
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card';
import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input';
import {MatTab, MatTabGroup} from '@angular/material/tabs';
import {NgIf} from '@angular/common';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {Tutorial} from '../../models/tutorial.model';
import {UserApiService} from '../user-api.service';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';
@Component({
selector: 'app-tutorial-edit.component',
imports: [
MarkdownComponent,
MatButton,
MatCard,
MatCardActions,
MatCardContent,
MatCardHeader,
MatFormField,
MatInput,
MatLabel,
MatTab,
MatTabGroup,
ReactiveFormsModule,
FormsModule,
MatFormField,
RouterLink,
MatError
],
templateUrl: './tutorial-edit.component.html',
styleUrl: './tutorial-edit.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class TutorialEditComponent {
tutorial: Tutorial = new Tutorial();
submitted = false;
hasError = false;
errorMessage = '';
markdown = `## Markdown __rulez__!
---
### Syntax highlight
\`\`\`typescript
const language = 'typescript';
\`\`\`
### Lists
1. Ordered list
2. Another bullet point
- Unordered list
- Another unordered bullet
### Blockquote
> Blockquote to the max`;
private id: string | null | undefined;
constructor(private userApiService: UserApiService, private route: ActivatedRoute) {
this.route.queryParams
.subscribe(params => {
console.log(params);
this.id = params['id'];
console.log(this.id);
});
this.userApiService.getTutorial(this.id).subscribe(
data=>{
this.tutorial = data;
}
);
}
updateTutorial(): void {
this.userApiService.update(this.id, this.tutorial)
.subscribe(
response => {
console.log(response);
this.submitted = true;
this.hasError = false;
},
error => {
console.log(error);
this.errorMessage = error.error.message;
this.hasError = true;
});
}
}
@@ -1,29 +1,148 @@
<p>tutorials-list.component works!</p>
<article class="table-header">
<button
class="button-remove-rows"
mat-button
(click)="removeSelectedRows()"
>
Remove Rows
</button>
<!--<button class="button-add-row" mat-button (click)="addRow()">
Add Row
</button>-->
<button mat-raised-button routerLink="../tutorial-add">Add tutorial</button>
<button mat-raised-button routerLink="../tutorial-add">Add tutorial</button>
</article>
<table mat-table [dataSource]="dataSource">
@for (column of columnsSchema; track column){
<ng-container [matColumnDef]="column.key">
<th mat-header-cell *matHeaderCellDef>
@switch (column.key) {
@case ('isSelected') {
<mat-checkbox
(change)="selectAll($event)"
[checked]="isAllSelected()"
[indeterminate]="!isAllSelected() && isAnySelected()"
></mat-checkbox>
}
@default {
{{ column.label }}
}
}
</th>
<td mat-cell *matCellDef="let element">
<!-- @if (element.isEdit) {-->
@switch (column.type) {
@case ('isSelected') {
<mat-checkbox
(change)="element.isSelected = $event.checked"
[checked]="element.isSelected"
></mat-checkbox>
}
@case('isEdit') {
<div class="btn-edit" >
<button mat-button routerLink='../tutorial-edit' [queryParams]="{id:element.id}" (click)="element.isEdit = !element.isEdit">
Edit
</button>
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
>
Delete
</button>
</div>
}
@case ('boolean') {
<mat-slide-toggle
class="example-margin"
[checked]="element[column.key]"
[disabled]="true">
<mat-list role="list">
@for (tutorial of tutorials; track tutorial) {
<!--
<mat-list-item role="listitem">
{{tutorial.title}}
</mat-slide-toggle>
}
@case ('datetime') {
{{ element[column.key] | date: 'medium' }}
}
@default {
{{ element[column.key] }}
}
}
<!-- }-->
</td>
</ng-container>
<span class="spacer"></span>
<button mat-button>
<mat-icon>edit</mat-icon>
</button>
<button mat-button>
<mat-icon>delete</mat-icon>
</button>
</mat-list-item>-->
<mat-option >
<div style="display:flex; justify-content: space-between">
<span>{{tutorial.title}}</span>
<span></span>
<span (click)="deleteOption($event, tutorial)">X</span>
</div>
</mat-option>
}
<!--<ng-container [matColumnDef]="col.key" *ngFor="let col of columnsSchema">-->
<!--<th mat-header-cell *matHeaderCellDef [ngSwitch]="col.key">
<span *ngSwitchCase="'isSelected'">
<mat-checkbox
(change)="selectAll($event)"
[checked]="isAllSelected()"
[indeterminate]="!isAllSelected() && isAnySelected()"
></mat-checkbox>
</span>
<span *ngSwitchDefault>{{ col.label }}</span>
</th>-->
<!-- <mat-form-field-->
<!--<td mat-cell *matCellDef="let element">
<div [ngSwitch]="col.type" *ngIf="!element.isEdit">
<ng-container *ngSwitchCase="'isSelected'">
<mat-checkbox
(change)="element.isSelected = $event.checked"
[checked]="element.isSelected"
></mat-checkbox>
</ng-container>
<div class="btn-edit" *ngSwitchCase="'isEdit'">
<button mat-button (click)="element.isEdit = !element.isEdit">
Edit
</button>
<button
mat-button
class="button-remove"
(click)="removeRow(element.id)"
>
Delete
</button>
</div>
<span *ngSwitchCase="'date'">
{{ element[col.key] | date: 'mediumDate' }}
</span>
<span *ngSwitchDefault>
{{ element[col.key] }}
</span>
</div>
<div [ngSwitch]="col.type" *ngIf="element.isEdit">
<div *ngSwitchCase="'isSelected'"></div>
<div class="btn-edit" *ngSwitchCase="'isEdit'">
<button
mat-button
(click)="editRow(element)"
[disabled]="disableSubmit(element.id)"
>
Done
</button>
</div>
class="form-input"
*ngSwitchCase="'date'"
appearance="fill"
>
<mat-label>Choose a date</mat-label>
<input
matInput
[matDatepicker]="picker"
[(ngModel)]="element[col.key]"
/>
<mat-datepicker-toggle
matSuffix
[for]="picker"
></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
</td>
</ng-container>-->
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-list>
@@ -1,3 +1,14 @@
.table-header {
width: 90%;
margin: auto;
text-align: right;
margin-bottom: 10px;
padding-top: 10px;
display: flex;
align-items: center;
justify-content: space-between;
}
.spacer{
flex: 1 1 auto;
}
@@ -1,6 +1,6 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit} from '@angular/core';
import {MatList, MatListItem} from '@angular/material/list';
import {Tutorial} from '../../models/tutorial.model';
import {Tutorial, TutorialColumns} from '../../models/tutorial.model';
import {TutorialService} from '../../services/tutorial.service';
import {UserApiService} from '../user-api.service';
import {MatButton} from '@angular/material/button';
@@ -11,29 +11,80 @@ import {EventBusService} from '../../_shared/event-bus.service';
import {EventData} from '../../_shared/event.class';
import {Router, RouterLink} from '@angular/router';
import {AuthService} from '../../services/auth.service';
import {FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {
MatCell,
MatCellDef,
MatColumnDef, MatHeaderCell, MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef,
MatTable,
MatTableDataSource
} from '@angular/material/table';
import {MatFormField, MatInput} from '@angular/material/input';
import {DatePipe, NgForOf, NgIf, NgSwitch, NgSwitchCase, NgSwitchDefault} from '@angular/common';
import {MatCheckbox} from '@angular/material/checkbox';
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from '@angular/material/datepicker';
import {MatSlideToggle} from '@angular/material/slide-toggle';
@Component({
selector: 'app-tutorials-list.component',
imports: [
MatList,
MatListItem,
MatLine,
MatButton,
MatIcon,
MatOption,
RouterLink
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './tutorials-list.component.html',
styleUrl: './tutorials-list.component.scss'
})
export class TutorialsListComponent implements OnInit {
tutorials?: Tutorial[];
@Component({
selector: 'app-tutorials-list.component',
imports: [
MatList,
MatListItem,
MatLine,
MatButton,
MatIcon,
MatOption,
RouterLink,
ReactiveFormsModule,
MatTable,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FormsModule,
MatInput,
MatFormField,
MatColumnDef,
MatHeaderCell,
MatHeaderCellDef,
NgSwitch,
NgSwitchCase,
NgSwitchDefault,
MatCellDef,
MatCheckbox,
MatDatepickerInput,
MatDatepickerToggle,
MatDatepicker,
DatePipe,
NgIf,
NgForOf,
MatCell,
MatSlideToggle
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './tutorials-list.component.html',
styleUrl: './tutorials-list.component.scss'
})
export class TutorialsListComponent implements OnInit {
//tutorials?: Tutorial[];
displayedColumns: string[] = TutorialColumns.map((col) => col.key)
columnsSchema: any = TutorialColumns
dataSource = new MatTableDataSource<Tutorial>()
valid: any = {}
constructor(private userApiService: UserApiService,
private storageService: TokenStorageService,
private eventBusService: EventBusService,
private router: Router, private authService: AuthService) {
private router: Router,
private authService: AuthService
) {
}
@@ -41,14 +92,13 @@ export class TutorialsListComponent implements OnInit {
this.eventBusService.on('logout', () => {
this.logout();
})
this.retrieveTutorials();
this.getTutorials();
}
retrieveTutorials(): void {
getTutorials(): void {
this.userApiService.getUserAllTutorials()
.subscribe(
data => {
this.tutorials = data;
.subscribe(( data:Tutorial[] ) => {
this.dataSource.data = data;
console.log(data);
},
error => {
@@ -65,9 +115,63 @@ export class TutorialsListComponent implements OnInit {
});
}
deleteOption($event: MouseEvent, option: any) {
selectAll(event: any) {
this.dataSource.data = this.dataSource.data.map((item) => ({
...item,
isSelected: event.checked
}));
}
isAllSelected() {
return this.dataSource.data.every((item) => item.isSelected)
}
isAnySelected() {
return this.dataSource.data.some((item) => item.isSelected)
}
removeSelectedRows() {
const selectedTutorials = this.dataSource.data.filter((u: Tutorial) => u.isSelected)
/*this.dialog
.open(ConfirmDialogComponent)
.afterClosed()
.subscribe((confirm) => {*/
// if (confirm) {
this.userApiService.deleteTutorials(selectedTutorials).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: Tutorial) => !u.isSelected
)
})
// }
// })
}
removeRow(id: number) {
this.userApiService.deleteTutorial(id).subscribe(() => {
this.dataSource.data = this.dataSource.data.filter(
(u: Tutorial) => u.id !== id,
)
})
}
editRow(row: Tutorial) {
/*if (row.id === 0) {
this.userService.addUser(row).subscribe((newUser: User) => {
row.id = newUser.id
row.isEdit = false
})
} else {
this.userService.updateUser(row).subscribe(() => (row.isEdit = false))
}*/
}
disableSubmit(id: number) {
if (this.valid[id]) {
return Object.values(this.valid[id]).some((item) => item === false)
}
return false
}
}
logout(): void {
this.authService.logout().subscribe({
next: res => {
@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs';
import {forkJoin, Observable} from 'rxjs';
import {Tutorial} from '../models/tutorial.model';
import {HttpClient, HttpHeaders} from '@angular/common/http';
@@ -21,7 +21,28 @@ export class UserApiService {
return this.http.get<Tutorial[]>(`${this.baseUrl}/tutorials`, this.httpOptions);
}
getTutorial(id: string | null | undefined): Observable<Tutorial> {
return this.http.get<Tutorial>(`${this.baseUrl}/tutorial-get/${id}`);
}
create(data: any): Observable<any> {
return this.http.post(`${this.baseUrl}/tutorial-add`, data);
}
update(id: any,data: any): Observable<any> {
return this.http.put(`${this.baseUrl}/tutorial-update/${id}`, data);
}
deleteTutorials(tutorials: Tutorial[]): Observable<Tutorial[]> {
return forkJoin(
tutorials.map((tutorial) =>
this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${tutorial.id}`)
)
);
}
deleteTutorial(id: number): Observable<Tutorial> {
return this.http.delete<Tutorial>(`${this.baseUrl}/tutorials/${id}`);
}
}
@@ -47,4 +47,6 @@ mat-sidenav-content{
}
.pc-container{
top: 0px;
padding-left: 5px;
padding-right: 5px;
}
@@ -20,6 +20,10 @@ const USER_ROUTES: Routes = [
path: 'tutorial-add',
loadComponent: () => import('../user-module/tutorial-add.component/tutorial-add.component').then((c) => c.TutorialAddComponent)
},
{
path: 'tutorial-edit',
loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent)
},
{
path: 'ai-models',
loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent)