Remove SCSS files related to components, layouts, and Bootstrap variables no longer in use
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
|
||||
<!-- html -->
|
||||
<nav aria-label="Breadcrumb" class="breadcrumb">
|
||||
@let breadcrumbs = breadcrumbs$ | async;
|
||||
<a mat-button [routerLink]="['/']">
|
||||
<mat-icon>home</mat-icon>
|
||||
</a>
|
||||
@if (breadcrumbs) {
|
||||
@for (bc of breadcrumbs; track bc.url; let last = $last) {
|
||||
|
||||
@if (!last) {
|
||||
<a mat-button [routerLink]="bc.url">
|
||||
<mat-icon class="sep">chevron_right</mat-icon>
|
||||
{{ bc.label }}</a>
|
||||
} @else {
|
||||
<a mat-button >
|
||||
<mat-icon class="sep">chevron_right</mat-icon>
|
||||
{{ bc.label }}</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</nav>
|
||||
<mat-divider></mat-divider>
|
||||
@@ -0,0 +1,15 @@
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: inherit;
|
||||
}
|
||||
.breadcrumb .sep {
|
||||
font-size: 16px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.breadcrumb a[mat-button] {
|
||||
min-width: auto;
|
||||
padding: 0 4px;
|
||||
text-transform: none;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BreadcrumbsComponent } from './breadcrumbs.component';
|
||||
|
||||
describe('BreadcrumbsComponent', () => {
|
||||
let component: BreadcrumbsComponent;
|
||||
let fixture: ComponentFixture<BreadcrumbsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BreadcrumbsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BreadcrumbsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {Component, CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||
import {ActivatedRoute, NavigationEnd, Router, RouterLink} from '@angular/router';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {AsyncPipe, NgForOf, NgIf} from '@angular/common';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {Observable, startWith} from 'rxjs';
|
||||
import {filter, map} from 'rxjs/operators';
|
||||
import {MatDivider} from '@angular/material/divider';
|
||||
|
||||
type Breadcrumb = { label: string; url: string };
|
||||
|
||||
@Component({
|
||||
selector: 'app-breadcrumbs',
|
||||
imports: [
|
||||
RouterLink,
|
||||
MatIcon,
|
||||
MatButton,
|
||||
NgIf,
|
||||
AsyncPipe,
|
||||
NgForOf,
|
||||
MatDivider
|
||||
],
|
||||
templateUrl: './breadcrumbs.component.html',
|
||||
styleUrl: './breadcrumbs.component.scss',
|
||||
schemas:[CUSTOM_ELEMENTS_SCHEMA,NO_ERRORS_SCHEMA]
|
||||
})
|
||||
export class BreadcrumbsComponent {
|
||||
|
||||
breadcrumbs$: Observable<Breadcrumb[]>;
|
||||
|
||||
constructor(private router: Router, private route: ActivatedRoute) {
|
||||
this.breadcrumbs$ = this.router.events.pipe(
|
||||
filter(e => e instanceof NavigationEnd),
|
||||
startWith(null),
|
||||
map(() => this.build(this.route.root))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private build(route: ActivatedRoute, url = '', crumbs: Breadcrumb[] = []): Breadcrumb[] {
|
||||
const children = route.children;
|
||||
if (!children || children.length === 0) return crumbs;
|
||||
|
||||
for (const child of children) {
|
||||
if (child.outlet !== 'primary') continue;
|
||||
|
||||
const routeURL = child.snapshot.url.map(s => s.path).join('/');
|
||||
if (routeURL) url += `/${routeURL}`;
|
||||
|
||||
const label = child.snapshot.data['breadcrumb'] as string | undefined;
|
||||
if (label) crumbs.push({ label, url });
|
||||
|
||||
return this.build(child, url, crumbs);
|
||||
}
|
||||
return crumbs;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
fileName="fileInput"
|
||||
(change)="selectFile($event)"
|
||||
|
||||
@@ -21,4 +21,22 @@ export class GlobalConstants {
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
|
||||
public static readonly MARKDOWN_EXAMPLE =`## 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`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
>
|
||||
</app-side-bar-admin>
|
||||
<div class="pc-container">
|
||||
<app-breadcrumbs></app-breadcrumbs>
|
||||
<router-outlet />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
|
||||
import {Router, RouterOutlet} from "@angular/router";
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {SideBarAdminComponent} from '../side-bar-admin.component/side-bar-admin.component';
|
||||
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
|
||||
@Component({
|
||||
selector: 'app-admin.component',
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
SideBarAdminComponent
|
||||
SideBarAdminComponent,
|
||||
BreadcrumbsComponent
|
||||
],
|
||||
schemas:[CUSTOM_ELEMENTS_SCHEMA],
|
||||
templateUrl: './admin.component.html',
|
||||
|
||||
@@ -5,23 +5,28 @@ const ADMIN_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: AdminComponent,
|
||||
|
||||
data:{breadcrumb: 'Admin'},
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'admin-welcome' },
|
||||
{
|
||||
path: 'admin-welcome',
|
||||
loadComponent: () => import('../admin-module/admin-welcome.component/admin-welcome.component').then((c) => c.AdminWelcomeComponent),
|
||||
data:{breadcrumb: 'Welcome'}
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
loadComponent: () => import('../admin-module/settings.component/settings.component').then((c) => c.SettingsComponent)
|
||||
loadComponent: () => import('../admin-module/settings.component/settings.component').then((c) => c.SettingsComponent),
|
||||
data:{breadcrumb: 'Settings'}
|
||||
},
|
||||
{
|
||||
path: 'system',
|
||||
loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent)
|
||||
loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent),
|
||||
data:{breadcrumb: 'System'}
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent)
|
||||
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent),
|
||||
data:{breadcrumb: 'Users'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+2
-1
@@ -28,10 +28,11 @@
|
||||
alt="" width="100%" height="100%">
|
||||
<mat-toolbar class="image-toolbar">
|
||||
<mat-toolbar-row>
|
||||
<span class="menu-spacer"></span>
|
||||
<button matMiniFab matTooltip="Dawnload image" aria-label="Download" (click)="downloadImage(image.url)">
|
||||
<mat-icon>download</mat-icon>
|
||||
</button>
|
||||
<span class="menu-spacer"></span>
|
||||
|
||||
@if (isLoggedIn) {
|
||||
<button matMiniFab matTooltip="Save to server" aria-label="Save" (click)="save(image.url)" >
|
||||
<mat-icon>upload_file</mat-icon>
|
||||
|
||||
@@ -29,5 +29,9 @@ mat-toolbar{
|
||||
.router_outlet{
|
||||
margin-top: 65px;
|
||||
height: 100%;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.component{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const MAIN_ROUTES: Routes =[
|
||||
path: 'tutorials',
|
||||
loadChildren: () =>
|
||||
import('../tutorials-module/tutorials.module').then((m) => m.TutorialsModule),
|
||||
|
||||
},
|
||||
{
|
||||
path: 'ai',
|
||||
@@ -25,6 +26,8 @@ const MAIN_ROUTES: Routes =[
|
||||
path: 'user',
|
||||
loadChildren: () =>
|
||||
import('../user-module/user.module').then((m) => m.UserModule),
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
path: 'moderator',
|
||||
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
<app-side-bar-moderator class="pc-sidebar" ></app-side-bar-moderator>
|
||||
<div class="pc-container">
|
||||
<app-breadcrumbs></app-breadcrumbs>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {RouterOutlet} from '@angular/router';
|
||||
import {SideBarModeratorComponent} from '../side-bar-moderator.component/side-bar-moderator.component';
|
||||
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +10,7 @@ import {SideBarModeratorComponent} from '../side-bar-moderator.component/side-ba
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
SideBarModeratorComponent,
|
||||
BreadcrumbsComponent,
|
||||
|
||||
],
|
||||
templateUrl: './moderator.component.html',
|
||||
|
||||
@@ -6,19 +6,23 @@ const MODERATOR_ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ModeratorComponent,
|
||||
|
||||
data:{breadcrumb: 'Moderator'},
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'moderator-welcome' }, // default redirect
|
||||
{
|
||||
path: 'moderator-welcome',
|
||||
loadComponent: () => import('../moderator-module/moderator-welcome.component/moderator-welcome.component').then((c) => c.ModeratorWelcomeComponent),
|
||||
data:{breadcrumb: 'Welcome'}
|
||||
},
|
||||
{
|
||||
path: 'tutorials-list',
|
||||
loadComponent: () => import('../moderator-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent)
|
||||
loadComponent: () => import('../moderator-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent),
|
||||
data:{breadcrumb: 'Tutorials List'}
|
||||
},
|
||||
{
|
||||
path: 'tutorial-preview',
|
||||
loadComponent: () => import('../moderator-module/tutorial-preview.component/tutorial-preview.component').then((c) => c.TutorialPreviewComponent)
|
||||
loadComponent: () => import('../moderator-module/tutorial-preview.component/tutorial-preview.component').then((c) => c.TutorialPreviewComponent),
|
||||
data:{breadcrumb: 'Tutorial Preview'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+68
-21
@@ -1,28 +1,75 @@
|
||||
<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="Preview">
|
||||
<markdown class="preview" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Edit">
|
||||
<textarea class="variable-textarea" [(ngModel)]="tutorial.body"></textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<form [formGroup]="form" novalidate>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label class="label-style">Title</mat-label>
|
||||
<input matInput
|
||||
formControlName="title"
|
||||
>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-tab label="Example">
|
||||
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
|
||||
<markdown class="variable-binding" [data]="markdown"></markdown>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<mat-label class="label-style">Title image</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<table class="example-full-width">
|
||||
<tr>
|
||||
<td>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label>Title image</mat-label>
|
||||
<input matInput
|
||||
formControlName="titleimage"
|
||||
[disabled]="true"
|
||||
>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
|
||||
<button mat-raised-button color="primary" (click)="openUploadImageDialog('100ms', '5ms')">Upload image</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@if(tutorial.titleimage){
|
||||
<div class="example-full-width content-center">
|
||||
<img style="width: 716px; height: 400px" src="{{tutorial.titleimage}}" alt="">
|
||||
</div>
|
||||
}
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Body</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Result">
|
||||
<markdown class="preview" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea"
|
||||
|
||||
formControlName = "body"
|
||||
(input)="onBodyChange($event)"
|
||||
>
|
||||
|
||||
</textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<!--<mat-tab label="Editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor style="color: #1a1a1a"
|
||||
textareaId="editor2"
|
||||
[options]="editorOptions"
|
||||
name="markdownText"
|
||||
[(ngModel)]="tutorial.body"
|
||||
(onFullscreenExit)="hidePreview()"
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
</mat-tab>-->
|
||||
<mat-tab label="Example">
|
||||
<textarea class="variable-textarea" [value]="markdown"></textarea>
|
||||
<markdown class="variable-binding" [data]="markdown"></markdown>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-actions>
|
||||
|
||||
<button matButton (click)="publishTutorial()">Publicate</button>
|
||||
|
||||
+11
@@ -19,6 +19,11 @@ mat-card-title{
|
||||
|
||||
.example-full-width {
|
||||
width: 100%;
|
||||
margin-bottom: inherit;
|
||||
|
||||
}
|
||||
.content-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,3 +62,9 @@ mat-card-title{
|
||||
border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));
|
||||
box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0));
|
||||
}
|
||||
|
||||
|
||||
.label-style{
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
+135
-16
@@ -1,5 +1,13 @@
|
||||
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {FormsModule} from "@angular/forms";
|
||||
import {Component, CUSTOM_ELEMENTS_SCHEMA, inject} from '@angular/core';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from "@angular/forms";
|
||||
import {MarkdownComponent} from "ngx-markdown";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from "@angular/material/card";
|
||||
@@ -8,6 +16,27 @@ import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||
import {ActivatedRoute, RouterLink} from "@angular/router";
|
||||
import {Tutorial} from '../../../models/tutorial.model';
|
||||
import {ModeratorApiService} from '../moderator-api.service';
|
||||
import {GlobalConstants} from '../../../global-constants';
|
||||
import {MatDivider} from '@angular/material/divider';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {
|
||||
DialogSelectImageComponent
|
||||
} from '../../user-module/dialog-select-image.component/dialog-select-image.component';
|
||||
import {
|
||||
DialogUploadImageComponent
|
||||
} from '../../user-module/dialog-upload-image.component/dialog-upload-image.component';
|
||||
|
||||
const nonBlank = (): ValidatorFn => (c: AbstractControl): ValidationErrors | null =>
|
||||
c.value && /\S/.test(c.value) ? null : { nonBlank: true };
|
||||
|
||||
type TutorialFormValue = {
|
||||
title: string;
|
||||
titleimage: string;
|
||||
body: string;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-tutorial-preview.component',
|
||||
@@ -27,7 +56,9 @@ import {ModeratorApiService} from '../moderator-api.service';
|
||||
MatTabGroup,
|
||||
RouterLink,
|
||||
MatError,
|
||||
MatFormField
|
||||
MatFormField,
|
||||
MatDivider,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
templateUrl: './tutorial-preview.component.html',
|
||||
styleUrl: './tutorial-preview.component.scss',
|
||||
@@ -38,22 +69,23 @@ export class TutorialPreviewComponent {
|
||||
submitted = false;
|
||||
hasError = false;
|
||||
errorMessage = '';
|
||||
markdown = `## Markdown __rulez__!
|
||||
---
|
||||
markdown = GlobalConstants.MARKDOWN_EXAMPLE;
|
||||
|
||||
### Syntax highlight
|
||||
\`\`\`typescript
|
||||
const language = 'typescript';
|
||||
\`\`\`
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
### Lists
|
||||
1. Ordered list
|
||||
2. Another bullet point
|
||||
- Unordered list
|
||||
- Another unordered bullet
|
||||
|
||||
### Blockquote
|
||||
> Blockquote to the max`;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
form = this.fb.nonNullable.group({
|
||||
|
||||
title: ['', [Validators.required, nonBlank()] ],
|
||||
body: ['', [Validators.required, nonBlank()] ],
|
||||
|
||||
titleimage: this.fb.nonNullable.control({ value: '', disabled: true }),
|
||||
|
||||
});
|
||||
|
||||
|
||||
private id: string | null | undefined;
|
||||
|
||||
@@ -69,11 +101,43 @@ const language = 'typescript';
|
||||
this.moderatorApiService.getTutorial(this.id).subscribe(
|
||||
data=>{
|
||||
this.tutorial = data;
|
||||
console.log(data);
|
||||
this.form.patchValue(this.fromModel(data));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Map Model -> Form value
|
||||
private fromModel(m: Tutorial): Partial<TutorialFormValue> {
|
||||
return {
|
||||
title: m.title ?? '',
|
||||
body: m.body ?? '',
|
||||
titleimage: m.titleimage ?? '',
|
||||
|
||||
};
|
||||
}
|
||||
// Map Form value -> Model (compose with existing model if you need to keep id, etc.)
|
||||
private toModel(): Tutorial {
|
||||
const v = this.form.getRawValue(); // TutorialFormValue
|
||||
return {
|
||||
...this.tutorial, // keep immutable fields like id
|
||||
title: v.title.trim(),
|
||||
body: v.body.trim(),
|
||||
//
|
||||
titleimage: v.titleimage,
|
||||
modified: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
publishTutorial(): void {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
this.tutorial = this.toModel();
|
||||
// send `updated` to your API here
|
||||
this.tutorial.published = true;
|
||||
this.moderatorApiService.publish(this.id, this.tutorial)
|
||||
.subscribe(
|
||||
@@ -90,4 +154,59 @@ const language = 'typescript';
|
||||
this.hasError = true;
|
||||
});
|
||||
}
|
||||
|
||||
onBodyChange($event: Event) {
|
||||
this.tutorial.body = this.form.get('body')?.value;
|
||||
}
|
||||
|
||||
openSelectImageDialog(enterAnimationDuration: string, exitAnimationDuration: string) {
|
||||
|
||||
let dialogSelectRef = this.dialog.open(DialogSelectImageComponent, {
|
||||
height: '500px',
|
||||
width: '600px',
|
||||
enterAnimationDuration,
|
||||
exitAnimationDuration,
|
||||
// data: {username: this.dialogLoginData.username, password: this.dialogLoginData.password}
|
||||
});
|
||||
dialogSelectRef.componentInstance.uploadClicked.subscribe(result => {
|
||||
dialogSelectRef.close();
|
||||
this.openUploadImageDialog(enterAnimationDuration, exitAnimationDuration);
|
||||
})
|
||||
|
||||
const dialogSelectSubscription = dialogSelectRef.componentInstance.selectClicked
|
||||
.subscribe(result => {
|
||||
console.log('Got the data!', result);
|
||||
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
});
|
||||
}
|
||||
|
||||
openUploadImageDialog(enterAnimationDuration: string, exitAnimationDuration: string) {
|
||||
let dialogSelectRef = this.dialog.open(DialogUploadImageComponent, {
|
||||
height: '500px',
|
||||
width: '600px',
|
||||
enterAnimationDuration,
|
||||
exitAnimationDuration,
|
||||
// data: {username: this.dialogLoginData.username, password: this.dialogLoginData.password}
|
||||
});
|
||||
dialogSelectRef.componentInstance.openSelectDialogClicked.subscribe(result => {
|
||||
dialogSelectRef.close();
|
||||
this.openSelectImageDialog(enterAnimationDuration, exitAnimationDuration);
|
||||
})
|
||||
|
||||
const dialogUploadSubscription = dialogSelectRef.componentInstance.selectUploadedImageClicked
|
||||
.subscribe(result => {
|
||||
console.log('Got the data!', result);
|
||||
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -1,10 +1,18 @@
|
||||
<div class="container-card-view">
|
||||
@for(tutorial of tutorials; track $index){
|
||||
<mat-card>
|
||||
<img mat-card-image [src]="tutorial.titleimage" alt="Photo of a" />
|
||||
<mat-card
|
||||
routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}"
|
||||
style="cursor: grab"
|
||||
>
|
||||
<mat-card-content>
|
||||
<img mat-card-image [src]="tutorial.titleimage" alt="Photo of a" />
|
||||
<!--<a routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}">
|
||||
|
||||
</a>-->
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions>
|
||||
<button mat-button routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}" target="_blank">{{tutorial.title}}</button>
|
||||
<a class="app-link" routerLink="../tutorial-view" [queryParams]="{id:tutorial.id}">{{tutorial.title}}</a>
|
||||
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
+36
-9
@@ -11,15 +11,7 @@
|
||||
object-fit: cover;
|
||||
padding: 24px;
|
||||
justify-content: center;
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
//padding-top: 20px;
|
||||
//display: flex;
|
||||
//flex-wrap: wrap;
|
||||
//flex-direction: row;
|
||||
//align-content: flex-start;
|
||||
//justify-content: space-around;
|
||||
//align-items: center;
|
||||
min-height: fit-content;
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -38,3 +30,38 @@ img {
|
||||
gap: 24px;
|
||||
|
||||
}
|
||||
|
||||
.app-link{
|
||||
font-weight: bold;
|
||||
color:cyan;
|
||||
}
|
||||
.app-link:link{
|
||||
|
||||
text-decoration:none;
|
||||
}
|
||||
.app-link:visited{
|
||||
//color:#ff0000;
|
||||
text-decoration:none;
|
||||
}
|
||||
.app-link:hover{
|
||||
//color:#ff0000;
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
.mat-mdc-card{
|
||||
background-color: rgba(153,153,153,0.16);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 0;
|
||||
}
|
||||
.mat-mdc-card-content {
|
||||
padding: 0;
|
||||
}
|
||||
.mat-mdc-card-content:first-child{
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.mat-mdc-card-actions{
|
||||
min-height: 64px;
|
||||
backdrop-filter: blur(8px);
|
||||
background-color: #1d284a85;
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {Tutorial} from '../../../models/tutorial.model';
|
||||
import {MatCard, MatCardActions, MatCardImage} from '@angular/material/card';
|
||||
import {MatCard, MatCardActions, MatCardContent, MatCardImage} from '@angular/material/card';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {TutorialsApiService} from '../tutorials-api.service';
|
||||
@@ -12,7 +12,8 @@ import {TutorialsApiService} from '../tutorials-api.service';
|
||||
MatCardActions,
|
||||
RouterLink,
|
||||
MatButton,
|
||||
MatCardImage
|
||||
MatCardImage,
|
||||
MatCardContent
|
||||
],
|
||||
templateUrl: './tutorials-card-view.component.html',
|
||||
styleUrl: './tutorials-card-view.component.scss'
|
||||
|
||||
+4
-1
@@ -1 +1,4 @@
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
|
||||
|
||||
+3
-1
@@ -6,4 +6,6 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
.component{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<!-- <mat-nav-list>-->
|
||||
@for (fileInfo of fileInfos; track fileInfo){
|
||||
<a mat-list-item (click)="select(fileInfo)">
|
||||
<img style="width: 120px; height: 120px" src="{{fileInfo.url}}" alt="">
|
||||
<img style="width: 179px; height: 100px" src="{{fileInfo.url}}" alt="">
|
||||
<span class="entry">
|
||||
|
||||
<!-- @if (!isCollapsed) {-->
|
||||
|
||||
+41
-52
@@ -1,26 +1,26 @@
|
||||
<mat-card appearance="outlined">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Add tutorial</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content >
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" novalidate>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label class="label-style">Title</mat-label>
|
||||
<input matInput required
|
||||
[(ngModel)]="tutorial.title">
|
||||
<input matInput
|
||||
formControlName="title"
|
||||
>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-label class="label-style">Title image</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<table class="example-full-width">
|
||||
<tr>
|
||||
<td style="width: 120px;">
|
||||
<img style="width: 120px; height: 120px" src="{{tutorial.titleimage}}" alt="">
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label>Title image</mat-label>
|
||||
<input matInput disabled
|
||||
[(ngModel)]="tutorial.titleimage">
|
||||
<input matInput
|
||||
|
||||
|
||||
formControlName="titleimage"
|
||||
[disabled]="true"
|
||||
>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
|
||||
@@ -30,50 +30,27 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Description</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.description"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Editor">
|
||||
<div class="markdown-editor-container">
|
||||
<div class="markdown-editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor
|
||||
textareaId="editor1"
|
||||
[options]="editorOptions"
|
||||
name="markdownText"
|
||||
[(ngModel)]="tutorial.description"
|
||||
(onFullscreenExit)="hidePreview()"
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@if(tutorial.titleimage){
|
||||
<div class="example-full-width content-center">
|
||||
<img style="width: 716px; height: 400px" src="{{tutorial.titleimage}}" alt="">
|
||||
</div>
|
||||
}
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Body</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea" [(ngModel)]="tutorial.body"></textarea>
|
||||
<textarea class="variable-textarea"
|
||||
|
||||
formControlName = "body"
|
||||
|
||||
(input)="onBodyChange($event)"
|
||||
>
|
||||
|
||||
</textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Editor">
|
||||
<!--<mat-tab label="Editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor style="color: #1a1a1a"
|
||||
textareaId="editor2"
|
||||
@@ -84,26 +61,38 @@
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
</mat-tab>
|
||||
</mat-tab>-->
|
||||
<mat-tab label="Result">
|
||||
<markdown class="preview" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Example">
|
||||
<textarea class="variable-textarea" [(ngModel)]="markdown"></textarea>
|
||||
<textarea class="variable-textarea" [value]="markdown"></textarea>
|
||||
<markdown class="variable-binding" [data]="markdown"></markdown>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-actions>
|
||||
@if (hasError){
|
||||
<mat-error>
|
||||
{{errorMessage}}
|
||||
</mat-error>
|
||||
}
|
||||
|
||||
@if (submitted){
|
||||
<div >
|
||||
<h4>Tutorial was submitted successfully!</h4>
|
||||
<button matButton (click)="newTutorial()">Add new tutorial</button>
|
||||
<button matButton routerLink="../tutorials-list">Back to list</button>
|
||||
</div>
|
||||
} @else {
|
||||
<button matButton (click)="saveTutorial()" >Save</button>
|
||||
<button matButton (click)="saveTutorial()"
|
||||
[disabled]="form.invalid"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
}
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
+4
-1
@@ -24,6 +24,9 @@ mat-card-title{
|
||||
.example-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.content-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.variable-binding,
|
||||
@@ -55,7 +58,7 @@ mat-card-title{
|
||||
}
|
||||
|
||||
.label-style{
|
||||
font-size: -webkit-xxx-large;
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
+114
-103
@@ -2,21 +2,44 @@ import {Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit} from '@angular/core';
|
||||
import {UserApiService} from '../user-api.service';
|
||||
import {TokenStorageService} from '../../../services/token-storage.service';
|
||||
import {EventBusService} from '../../../_shared/event-bus.service';
|
||||
import {Router} from '@angular/router';
|
||||
import {Router, RouterLink} from '@angular/router';
|
||||
import {AuthService} from '../../../services/auth.service';
|
||||
import {Tutorial} from '../../../models/tutorial.model';
|
||||
import {MarkdownComponent, MarkdownService} from 'ngx-markdown';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {MatCard, MatCardActions, MatCardContent, MatCardHeader} from '@angular/material/card';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import {MatError, MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import {MatTab, MatTabGroup} from '@angular/material/tabs';
|
||||
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule, ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {AngularMarkdownEditorModule, EditorInstance, EditorOption} from 'angular-markdown-editor';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component';
|
||||
import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
|
||||
import {FileUploadComponent} from '../../../components/file-upload.component/file-upload.component';
|
||||
import {MatDivider} from '@angular/material/divider';
|
||||
import {GlobalConstants} from '../../../global-constants';
|
||||
|
||||
|
||||
const nonBlank = (): ValidatorFn => (c: AbstractControl): ValidationErrors | null =>
|
||||
c.value && /\S/.test(c.value) ? null : { nonBlank: true };
|
||||
|
||||
type TutorialFormValue = {
|
||||
title: string;
|
||||
titleimage: string;
|
||||
body: string;
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-tutorial-add.component',
|
||||
@@ -35,140 +58,122 @@ import {MatDivider} from '@angular/material/divider';
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
AngularMarkdownEditorModule,
|
||||
MatDivider
|
||||
MatDivider,
|
||||
MatError,
|
||||
RouterLink
|
||||
],
|
||||
templateUrl: './tutorial-add.component.html',
|
||||
styleUrl: './tutorial-add.component.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class TutorialAddComponent implements OnInit{
|
||||
export class TutorialAddComponent{
|
||||
|
||||
tutorial: Tutorial = new Tutorial();
|
||||
submitted = false;
|
||||
markdownText = '';
|
||||
showEditor = true;
|
||||
bsEditorInstance!: EditorInstance;
|
||||
tutorialForm!: FormGroup;
|
||||
editorOptions!: EditorOption;
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
markdown = GlobalConstants.MARKDOWN_EXAMPLE;
|
||||
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
markdown = `## Markdown __rulez__!
|
||||
---
|
||||
form = this.fb.nonNullable.group({
|
||||
|
||||
### Syntax highlight
|
||||
\`\`\`typescript
|
||||
const language = 'typescript';
|
||||
\`\`\`
|
||||
title: ['', [Validators.required, nonBlank()] ],
|
||||
body: ['', [Validators.required, nonBlank()] ],
|
||||
|
||||
### Lists
|
||||
1. Ordered list
|
||||
2. Another bullet point
|
||||
- Unordered list
|
||||
- Another unordered bullet
|
||||
titleimage: this.fb.nonNullable.control({ value: '', disabled: true }),
|
||||
|
||||
### Blockquote
|
||||
> Blockquote to the max`;
|
||||
});
|
||||
|
||||
hasError = false;
|
||||
errorMessage = '';
|
||||
|
||||
constructor(private fb: FormBuilder,
|
||||
constructor(
|
||||
private markdownService: MarkdownService,
|
||||
private userApiService: UserApiService,
|
||||
) {
|
||||
|
||||
this.tutorial.description = this.markdown;
|
||||
this.tutorial.title="";
|
||||
this.tutorial.titleimage="";
|
||||
this.tutorial.body = this.markdown;
|
||||
|
||||
this.form.patchValue(this.fromModel(this.tutorial));
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.editorOptions = {
|
||||
autofocus: false,
|
||||
iconlibrary: 'fa',
|
||||
height: 300,
|
||||
savable: false,
|
||||
onFullscreenExit: (e) => this.hidePreview(),
|
||||
onShow: (e) => this.bsEditorInstance = e,
|
||||
parser: (val) => this.parse(val)
|
||||
// Map Model -> Form value
|
||||
private fromModel(m: Tutorial): Partial<TutorialFormValue> {
|
||||
return {
|
||||
title: m.title ?? '',
|
||||
body: m.body ?? '',
|
||||
titleimage: m.titleimage ?? '',
|
||||
|
||||
};
|
||||
this.buildForm(this.tutorial.description);
|
||||
|
||||
}
|
||||
|
||||
buildForm(markdownText: string | undefined) {
|
||||
this.tutorialForm = this.fb.group({
|
||||
body: [markdownText],
|
||||
isPreview: [true]
|
||||
});
|
||||
}
|
||||
|
||||
/** highlight all code found, needs to be wrapped in timer to work properly */
|
||||
highlight() {
|
||||
setTimeout(() => {
|
||||
this.markdownService.highlight();
|
||||
});
|
||||
}
|
||||
|
||||
hidePreview() {
|
||||
if (this.bsEditorInstance && this.bsEditorInstance.hidePreview) {
|
||||
this.bsEditorInstance.hidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
showFullScreen(isFullScreen: boolean) {
|
||||
if (this.bsEditorInstance && this.bsEditorInstance.setFullscreen) {
|
||||
this.bsEditorInstance.showPreview();
|
||||
this.bsEditorInstance.setFullscreen(isFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
parse(inputValue: string) {
|
||||
const markedOutput = this.markdownService.parse(inputValue.trim());
|
||||
this.highlight();
|
||||
|
||||
return markedOutput;
|
||||
}
|
||||
|
||||
onFormChanges(): void {
|
||||
this.tutorialForm.valueChanges.subscribe(formData => {
|
||||
if (formData) {
|
||||
this.markdownText = formData.body;
|
||||
}
|
||||
});
|
||||
// Map Form value -> Model (compose with existing model if you need to keep id, etc.)
|
||||
private toModel(): Tutorial {
|
||||
const v = this.form.getRawValue(); // TutorialFormValue
|
||||
return {
|
||||
...this.tutorial, // keep immutable fields like id
|
||||
title: v.title.trim(),
|
||||
body: v.body.trim(),
|
||||
//
|
||||
titleimage: v.titleimage,
|
||||
created: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
saveTutorial(): void {
|
||||
const data = {
|
||||
title: this.tutorial.title,
|
||||
description: this.tutorial.description,
|
||||
body: this.tutorial.body,
|
||||
published: this.tutorial.published,
|
||||
titleimage: this.tutorial.titleimage,
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
tobepublished: false,
|
||||
isEdit: false,
|
||||
isAdmin: false,
|
||||
isSuperAdmin: false,
|
||||
isUser: false,
|
||||
};
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
this.tutorial = this.toModel();
|
||||
// send `updated` to your API here
|
||||
this.userApiService.create(this.tutorial)
|
||||
.subscribe(
|
||||
response => {
|
||||
console.log(response);
|
||||
this.submitted = true;
|
||||
},
|
||||
)
|
||||
|
||||
const data = {
|
||||
title: this.tutorial.title,
|
||||
description: this.tutorial.description,
|
||||
body: this.tutorial.body,
|
||||
published: this.tutorial.published,
|
||||
titleimage: this.tutorial.titleimage,
|
||||
created: new Date(),
|
||||
modified: new Date(),
|
||||
tobepublished: false,
|
||||
isEdit: false,
|
||||
isAdmin: false,
|
||||
isSuperAdmin: false,
|
||||
isUser: false,
|
||||
}
|
||||
|
||||
this.userApiService.create(data)
|
||||
.subscribe(
|
||||
response => {
|
||||
console.log(response);
|
||||
this.submitted = true;
|
||||
this.hasError = false;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
this.errorMessage = error.error.message;
|
||||
|
||||
this.hasError = true;
|
||||
});
|
||||
|
||||
this.userApiService.create(data)
|
||||
.subscribe(
|
||||
response => {
|
||||
console.log(response);
|
||||
this.submitted = true;
|
||||
},
|
||||
error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
newTutorial(): void {
|
||||
this.submitted = false;
|
||||
this.tutorial = {
|
||||
title: '',
|
||||
description: '',
|
||||
body:"",
|
||||
published: false
|
||||
};
|
||||
}
|
||||
@@ -195,6 +200,7 @@ const language = 'typescript';
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
|
||||
});
|
||||
}
|
||||
@@ -220,7 +226,12 @@ const language = 'typescript';
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
});
|
||||
}
|
||||
|
||||
onBodyChange($event: Event) {
|
||||
this.tutorial.body = this.form.get('body')?.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+64
-90
@@ -1,104 +1,78 @@
|
||||
<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 class="label-style">Title</mat-label>
|
||||
<input matInput required
|
||||
[(ngModel)]="tutorial.title">
|
||||
</mat-form-field>
|
||||
<form [formGroup]="form" novalidate>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label class="label-style">Title</mat-label>
|
||||
<input matInput
|
||||
formControlName="title"
|
||||
>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-label class="label-style">Title image</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<table class="example-full-width">
|
||||
<tr>
|
||||
<td style="width: 120px;">
|
||||
<img style="width: 120px; height: 120px" src="{{tutorial.titleimage}}" alt="">
|
||||
</td>
|
||||
<td>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label>Title image</mat-label>
|
||||
<input matInput disabled
|
||||
[(ngModel)]="tutorial.titleimage">
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
|
||||
<button mat-raised-button color="primary" (click)="openUploadImageDialog('100ms', '5ms')">Upload image</button>
|
||||
</div>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Description</mat-label>
|
||||
<mat-label class="label-style">Title image</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea" [(ngModel)]="tutorial.description"></textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.description"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Editor">
|
||||
<div class="markdown-editor-container">
|
||||
<div class="markdown-editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor
|
||||
textareaId="editor1"
|
||||
[options]="editorOptions"
|
||||
name="markdownText"
|
||||
[(ngModel)]="tutorial.description"
|
||||
(onFullscreenExit)="hidePreview()"
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
<table class="example-full-width">
|
||||
<tr>
|
||||
<td>
|
||||
<mat-form-field class="example-full-width">
|
||||
<mat-label>Title image</mat-label>
|
||||
<input matInput
|
||||
formControlName="titleimage"
|
||||
[disabled]="true"
|
||||
>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-raised-button color="primary" (click)="openSelectImageDialog('100ms', '5ms')">Select image</button>
|
||||
<button mat-raised-button color="primary" (click)="openUploadImageDialog('100ms', '5ms')">Upload image</button>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@if(tutorial.titleimage){
|
||||
<div class="example-full-width content-center">
|
||||
<img style="width: 716px; height: 400px" src="{{tutorial.titleimage}}" alt="">
|
||||
</div>
|
||||
}
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Body</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea"
|
||||
|
||||
<div class="example-full-width">
|
||||
<mat-label class="label-style">Body</mat-label>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Markdown text | Result">
|
||||
<textarea class="variable-textarea" [(ngModel)]="tutorial.body"></textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor style="color: #1a1a1a"
|
||||
textareaId="editor2"
|
||||
[options]="editorOptions"
|
||||
name="markdownText"
|
||||
[(ngModel)]="tutorial.body"
|
||||
(onFullscreenExit)="hidePreview()"
|
||||
formControlName = "body"
|
||||
(input)="onBodyChange($event)"
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
</mat-tab>
|
||||
<mat-tab label="Result">
|
||||
<markdown class="preview" [data]="tutorial.body"></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>
|
||||
</div>
|
||||
|
||||
</textarea>
|
||||
<markdown class="variable-binding" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<!--<mat-tab label="Editor">
|
||||
<form novalidate>
|
||||
<angular-markdown-editor style="color: #1a1a1a"
|
||||
textareaId="editor2"
|
||||
[options]="editorOptions"
|
||||
name="markdownText"
|
||||
[(ngModel)]="tutorial.body"
|
||||
(onFullscreenExit)="hidePreview()"
|
||||
>
|
||||
</angular-markdown-editor>
|
||||
</form>
|
||||
</mat-tab>-->
|
||||
<mat-tab label="Result">
|
||||
<markdown class="preview" [data]="tutorial.body"></markdown>
|
||||
</mat-tab>
|
||||
<mat-tab label="Example">
|
||||
<textarea class="variable-textarea" [value]="markdown"></textarea>
|
||||
<markdown class="variable-binding" [data]="markdown"></markdown>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-card-actions>
|
||||
|
||||
<button matButton (click)="updateTutorial()">Save</button>
|
||||
<button matButton (click)="updateTutorial()" [disabled]="form.invalid">Save</button>
|
||||
@if (hasError){
|
||||
<mat-error>
|
||||
{{errorMessage}}
|
||||
|
||||
+11
@@ -19,6 +19,11 @@ mat-card-title{
|
||||
|
||||
.example-full-width {
|
||||
width: 100%;
|
||||
margin-bottom: inherit;
|
||||
|
||||
}
|
||||
.content-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,3 +62,9 @@ mat-card-title{
|
||||
border-color: var(--mat-card-outlined-outline-color, var(--mat-sys-outline-variant));
|
||||
box-shadow: var(--mat-card-outlined-container-elevation, var(--mat-sys-level0));
|
||||
}
|
||||
|
||||
|
||||
.label-style{
|
||||
font-size: xx-large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
+66
-73
@@ -4,7 +4,15 @@ 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 {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule, ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import {Tutorial} from '../../../models/tutorial.model';
|
||||
import {UserApiService} from '../user-api.service';
|
||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||
@@ -13,6 +21,18 @@ import {MatDivider} from '@angular/material/divider';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {DialogSelectImageComponent} from '../dialog-select-image.component/dialog-select-image.component';
|
||||
import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialog-upload-image.component';
|
||||
import {GlobalConstants} from '../../../global-constants';
|
||||
|
||||
const nonBlank = (): ValidatorFn => (c: AbstractControl): ValidationErrors | null =>
|
||||
c.value && /\S/.test(c.value) ? null : { nonBlank: true };
|
||||
|
||||
type TutorialFormValue = {
|
||||
title: string;
|
||||
titleimage: string;
|
||||
body: string;
|
||||
|
||||
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-tutorial-edit.component',
|
||||
@@ -40,41 +60,34 @@ import {DialogUploadImageComponent} from '../dialog-upload-image.component/dialo
|
||||
styleUrl: './tutorial-edit.component.scss',
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class TutorialEditComponent implements OnInit{
|
||||
export class TutorialEditComponent{
|
||||
|
||||
tutorial: Tutorial = new Tutorial();
|
||||
submitted = false;
|
||||
|
||||
hasError = false;
|
||||
errorMessage = '';
|
||||
markdownText="";
|
||||
bsEditorInstance!: EditorInstance;
|
||||
tutorialForm!: FormGroup;
|
||||
editorOptions!: EditorOption;
|
||||
|
||||
readonly dialog = inject(MatDialog);
|
||||
|
||||
|
||||
markdown = `## Markdown __rulez__!
|
||||
---
|
||||
markdown = GlobalConstants.MARKDOWN_EXAMPLE;
|
||||
|
||||
### Syntax highlight
|
||||
\`\`\`typescript
|
||||
const language = 'typescript';
|
||||
\`\`\`
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
### Lists
|
||||
1. Ordered list
|
||||
2. Another bullet point
|
||||
- Unordered list
|
||||
- Another unordered bullet
|
||||
form = this.fb.nonNullable.group({
|
||||
|
||||
title: ['', [Validators.required, nonBlank()] ],
|
||||
body: ['', [Validators.required, nonBlank()] ],
|
||||
|
||||
titleimage: this.fb.nonNullable.control({ value: '', disabled: true }),
|
||||
|
||||
});
|
||||
|
||||
### Blockquote
|
||||
> Blockquote to the max`;
|
||||
private id: string | null | undefined;
|
||||
|
||||
constructor(private userApiService: UserApiService,
|
||||
private route: ActivatedRoute,
|
||||
private fb: FormBuilder,
|
||||
private markdownService: MarkdownService
|
||||
) {
|
||||
|
||||
@@ -88,66 +101,42 @@ const language = 'typescript';
|
||||
this.userApiService.getTutorial(this.id).subscribe(
|
||||
data=>{
|
||||
this.tutorial = data;
|
||||
this.form.patchValue(this.fromModel(this.tutorial));
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.editorOptions = {
|
||||
autofocus: false,
|
||||
iconlibrary: 'fa',
|
||||
height: 300,
|
||||
savable: false,
|
||||
onFullscreenExit: (e) => this.hidePreview(),
|
||||
onShow: (e) => this.bsEditorInstance = e,
|
||||
parser: (val) => this.parse(val)
|
||||
// Map Model -> Form value
|
||||
private fromModel(m: Tutorial): Partial<TutorialFormValue> {
|
||||
return {
|
||||
title: m.title ?? '',
|
||||
body: m.body ?? '',
|
||||
titleimage: m.titleimage ?? '',
|
||||
|
||||
};
|
||||
this.buildForm(this.tutorial.description);
|
||||
|
||||
}
|
||||
|
||||
buildForm(markdownText: string | undefined) {
|
||||
this.tutorialForm = this.fb.group({
|
||||
body: [markdownText],
|
||||
isPreview: [true]
|
||||
});
|
||||
}
|
||||
/** highlight all code found, needs to be wrapped in timer to work properly */
|
||||
highlight() {
|
||||
setTimeout(() => {
|
||||
this.markdownService.highlight();
|
||||
});
|
||||
}
|
||||
|
||||
hidePreview() {
|
||||
if (this.bsEditorInstance && this.bsEditorInstance.hidePreview) {
|
||||
this.bsEditorInstance.hidePreview();
|
||||
}
|
||||
}
|
||||
|
||||
showFullScreen(isFullScreen: boolean) {
|
||||
if (this.bsEditorInstance && this.bsEditorInstance.setFullscreen) {
|
||||
this.bsEditorInstance.showPreview();
|
||||
this.bsEditorInstance.setFullscreen(isFullScreen);
|
||||
}
|
||||
}
|
||||
|
||||
parse(inputValue: string) {
|
||||
const markedOutput = this.markdownService.parse(inputValue.trim());
|
||||
this.highlight();
|
||||
|
||||
return markedOutput;
|
||||
}
|
||||
|
||||
onFormChanges(): void {
|
||||
this.tutorialForm.valueChanges.subscribe(formData => {
|
||||
if (formData) {
|
||||
this.markdownText = formData.body;
|
||||
}
|
||||
});
|
||||
// Map Form value -> Model (compose with existing model if you need to keep id, etc.)
|
||||
private toModel(): Tutorial {
|
||||
const v = this.form.getRawValue(); // TutorialFormValue
|
||||
return {
|
||||
...this.tutorial, // keep immutable fields like id
|
||||
title: v.title.trim(),
|
||||
body: v.body.trim(),
|
||||
//
|
||||
titleimage: v.titleimage,
|
||||
modified: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
updateTutorial(): void {
|
||||
if (this.form.invalid) {
|
||||
this.form.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
this.tutorial = this.toModel();
|
||||
// send `updated` to your API here
|
||||
this.userApiService.update(this.id, this.tutorial)
|
||||
.subscribe(
|
||||
response => {
|
||||
@@ -186,7 +175,7 @@ const language = 'typescript';
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,7 +200,11 @@ const language = 'typescript';
|
||||
return;
|
||||
}
|
||||
this.tutorial.titleimage = result.url;
|
||||
|
||||
this.form.patchValue({ titleimage: result.url });
|
||||
});
|
||||
}
|
||||
|
||||
onBodyChange($event: Event) {
|
||||
this.tutorial.body = this.form.get('body')?.value;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,4 +1,6 @@
|
||||
|
||||
<article class="table-header">
|
||||
<button mat-raised-button routerLink="../tutorial-add">Add tutorial</button>
|
||||
<button
|
||||
class="button-remove-rows"
|
||||
mat-button
|
||||
@@ -6,7 +8,7 @@
|
||||
>
|
||||
Remove Rows
|
||||
</button>
|
||||
<button mat-raised-button routerLink="../tutorial-add">Add tutorial</button>
|
||||
|
||||
|
||||
</article>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ import {
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {MatCheckbox} from '@angular/material/checkbox';
|
||||
import {MatSlideToggle} from '@angular/material/slide-toggle';
|
||||
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
|
||||
|
||||
|
||||
@Component({
|
||||
@@ -43,7 +44,8 @@ import {MatSlideToggle} from '@angular/material/slide-toggle';
|
||||
MatCheckbox,
|
||||
DatePipe,
|
||||
MatCell,
|
||||
MatSlideToggle
|
||||
MatSlideToggle,
|
||||
BreadcrumbsComponent
|
||||
],
|
||||
schemas:[CUSTOM_ELEMENTS_SCHEMA],
|
||||
templateUrl: './tutorials-list.component.html',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
</app-side-bar-user>
|
||||
|
||||
<div class="pc-container">
|
||||
<app-breadcrumbs></app-breadcrumbs>
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
//---------------
|
||||
.pc-sidebar{
|
||||
top: 65px;
|
||||
overflow-y: auto;
|
||||
background-color: rgba(153, 153, 153, 0.16);
|
||||
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.pc-container{
|
||||
top: 0px;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ import {RouterOutlet} from '@angular/router';
|
||||
import {MatSidenav} from '@angular/material/sidenav';
|
||||
import {BreakpointObserver} from '@angular/cdk/layout';
|
||||
import {SideBarUserComponent} from '../side-bar-user.component/side-bar-user.component';
|
||||
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user.component',
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
SideBarUserComponent
|
||||
SideBarUserComponent,
|
||||
BreadcrumbsComponent
|
||||
],
|
||||
templateUrl: './user.component.html',
|
||||
styleUrl: './user.component.scss',
|
||||
|
||||
@@ -7,30 +7,39 @@ const USER_ROUTES: Routes = [
|
||||
path: '',
|
||||
component: UserComponent,
|
||||
|
||||
data:{breadcrumb: 'User'},
|
||||
children: [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'user-welcome' }, // default redirect
|
||||
|
||||
{
|
||||
path: 'user-welcome',
|
||||
loadComponent: () => import('../user-module/user-welcome.component/user-welcome.component').then((c) => c.UserWelcomeComponent),
|
||||
data:{breadcrumb: 'Welcome'}
|
||||
},
|
||||
{
|
||||
path: 'tutorials-list',
|
||||
loadComponent: () => import('../user-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent)
|
||||
loadComponent: () => import('../user-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent),
|
||||
data:{breadcrumb: 'Tutorials List'}
|
||||
},
|
||||
{
|
||||
path: 'tutorial-add',
|
||||
loadComponent: () => import('../user-module/tutorial-add.component/tutorial-add.component').then((c) => c.TutorialAddComponent)
|
||||
loadComponent: () => import('../user-module/tutorial-add.component/tutorial-add.component').then((c) => c.TutorialAddComponent),
|
||||
data:{breadcrumb: 'Add Tutorial'}
|
||||
},
|
||||
{
|
||||
path: 'tutorial-edit',
|
||||
loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent)
|
||||
loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent),
|
||||
data:{breadcrumb: 'Edit Tutorial'}
|
||||
},
|
||||
{
|
||||
path: 'ai-models',
|
||||
loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent)
|
||||
loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent),
|
||||
data:{breadcrumb: 'AI'}
|
||||
},
|
||||
{
|
||||
path: 'images',
|
||||
loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent)
|
||||
loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent),
|
||||
data:{breadcrumb: 'Images'}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user