remake init data

json dump
This commit is contained in:
liosha84
2025-08-14 18:50:16 +03:00
parent 6c5e2d9de1
commit b5e99b0116
82 changed files with 2814 additions and 457 deletions
@@ -1,4 +1,4 @@
<mat-divider></mat-divider>
<!-- html -->
<nav aria-label="Breadcrumb" class="breadcrumb">
@let breadcrumbs = breadcrumbs$ | async;
@@ -3,6 +3,7 @@
align-items: center;
gap: 4px;
margin-bottom: inherit;
padding-left: 25px;
}
.breadcrumb .sep {
font-size: 16px;
@@ -12,4 +13,5 @@
min-width: auto;
padding: 0 4px;
text-transform: none;
color:cyan;
}
@@ -1,7 +1,7 @@
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 {AsyncPipe} from '@angular/common';
import {MatIcon} from '@angular/material/icon';
import {Observable, startWith} from 'rxjs';
import {filter, map} from 'rxjs/operators';
@@ -15,9 +15,9 @@ type Breadcrumb = { label: string; url: string };
RouterLink,
MatIcon,
MatButton,
NgIf,
AsyncPipe,
NgForOf,
MatDivider
],
templateUrl: './breadcrumbs.component.html',
@@ -47,7 +47,7 @@ export class BreadcrumbsComponent {
const routeURL = child.snapshot.url.map(s => s.path).join('/');
if (routeURL) url += `/${routeURL}`;
const label = child.snapshot.data['breadcrumb'] as string | undefined;
const label = child.snapshot.data['title'] as string | undefined;
if (label) crumbs.push({ label, url });
return this.build(child, url, crumbs);
@@ -0,0 +1,17 @@
<app-vertical-nav
[routes]="routes"
[basePath]="basePath"
(collapsedChange)="navCollapsedChanged($event)">
</app-vertical-nav>
<div class="app-breadcrumb" [class.collapsed]="isCollapsed">
<app-breadcrumbs></app-breadcrumbs>
</div>
<div class="app-container" [class.collapsed]="isCollapsed">
<div class="page-component">
<router-outlet />
</div>
</div>
@@ -0,0 +1,36 @@
.app-container{
top: -12px;
padding-left: 5px;
padding-right: 5px;
margin-left: 240px;
padding-top: 1px;
position: relative;
&.collapsed{
margin-left: 64px;
}
}
.app-breadcrumb{
position: fixed;
left: 240px;
width: 100%;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
z-index: 100;
&.collapsed{
left: 64px;
}
}
.page-component{
height: 100%;
margin-top: 65px;
margin-bottom: 50px;
margin-left: 10px;
margin-right: 10px;
background: aliceblue;
position: relative;
padding: 20px;
}
@@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SideBarAdminComponent } from './side-bar-admin.component';
import { InnerLayoutComponent } from './inner-layout.component';
describe('SideBarAdminComponent', () => {
let component: SideBarAdminComponent;
let fixture: ComponentFixture<SideBarAdminComponent>;
describe('InnerLayoutComponent', () => {
let component: InnerLayoutComponent;
let fixture: ComponentFixture<InnerLayoutComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SideBarAdminComponent]
imports: [InnerLayoutComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SideBarAdminComponent);
fixture = TestBed.createComponent(InnerLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
@@ -0,0 +1,39 @@
import {Component, Input, OnInit} from '@angular/core';
import {BreadcrumbsComponent} from "../breadcrumbs.component/breadcrumbs.component";
import {Route, RouterOutlet} from "@angular/router";
import {VerticalNavComponent} from "../vertical-nav.component/vertical-nav.component";
import {ADMIN_ROUTES} from '../../modules/admin-module/admin.routing';
@Component({
selector: 'app-inner-layout',
imports: [
BreadcrumbsComponent,
RouterOutlet,
VerticalNavComponent
],
templateUrl: './inner-layout.component.html',
styleUrl: './inner-layout.component.scss'
})
export class InnerLayoutComponent implements OnInit{
//adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']);
//adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']);
routes:Route[] = [];
@Input() moduleRoutes:Route[] = [];
// If your admin module is mounted at '/admin' in the app routes, keep this as '/admin'.
// If it's at root, set this to ''.
@Input() basePath = '';
isCollapsed = false;
ngOnInit(): void {
this.routes = (this.moduleRoutes[0]?.children ?? []).filter(r => r.path && r.data?.['nav']);
}
navCollapsedChanged($event: boolean) {
this.isCollapsed = $event;
}
}
@@ -0,0 +1,25 @@
<div class="nav-container" [class.collapsed]="!opened()">
<div class="nav-header">
<button mat-icon-button (click)="toggle()" aria-label="Toggle sidebar">
<mat-icon>{{ opened() ? 'chevron_left' : 'chevron_right' }}</mat-icon>
</button>
</div>
<mat-nav-list class="nav-list">
@for (item of (items || navItems()); track item) {
<a
mat-list-item
[routerLink]="item.path"
[routerLinkActive]="'active'"
[routerLinkActiveOptions]="{ exact: item.exact !== false }"
[matTooltip]="opened() ? '' : item.title"
matTooltipPosition="right"
>
<mat-icon matListItemIcon>{{ item.icon }}</mat-icon>
<span matListItemTitle class="label">{{ item.title }}</span>
</a>
}
</mat-nav-list>
</div>
@@ -0,0 +1,68 @@
.nav-container {
width: 240px;
height: 100%;
display: flex;
flex-direction: column;
border-right: 1px solid var(--mat-sys-outline-variant, rgba(0,0,0,0.12));
background-color: rgba(153,153,153,0.16);
backdrop-filter: blur(8px);
position: fixed;
z-index: 100;
&.collapsed {
width: 64px;
.label {
display: none;
}
}
}
.nav-header {
display: flex;
justify-content: flex-end;
// border-bottom: 1px solid var(--mat-sys-outline-variant, rgba(0,0,0,0.12));
padding-bottom: 1px;
}
a.mat-mdc-list-item {
height: 44px;
border-radius: 8px;
margin: 4px 4px;
color: cyan;
&.active {
background: color-mix(in oklab, var(--mat-sys-primary, #3f51b5) 16%, transparent);
}
}
.nav-list{
//background-color: #f5f7fb; /* your color */
background-color: rgba(153,153,153,0.16);
backdrop-filter: blur(8px);
}
.nav-list .mdc-list-item {
background-color: rgba(153,153,153,0.16);
backdrop-filter: blur(8px);
}
.nav-list .mdc-list-item:hover,
.nav-list .mdc-list-item:focus {
background-color: rgba(153,153,153,0.16);
backdrop-filter: blur(8px);
}
.nav-list .mdc-list-item--selected {
background-color: rgba(153, 153, 153, 0.50);
backdrop-filter: blur(8px);
}
/* Primary and secondary text */
.nav-list .mdc-list-item__primary-text,
.nav-list .mdc-list-item__secondary-text {
color: cyan;
}
/* Optional: icons inside list items */
.nav-list .mat-icon {
color: cyan;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VerticalNavComponent } from './vertical-nav.component';
describe('VerticalNavComponent', () => {
let component: VerticalNavComponent;
let fixture: ComponentFixture<VerticalNavComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VerticalNavComponent]
})
.compileComponents();
fixture = TestBed.createComponent(VerticalNavComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,111 @@
import {
Component,
computed,
CUSTOM_ELEMENTS_SCHEMA,
EventEmitter,
inject,
Input,
OnInit,
Output,
signal
} from '@angular/core';
import {Route, Router, RouterLink, RouterLinkActive} from '@angular/router';
import {MatListItem, MatListItemIcon, MatListItemTitle, MatNavList} from '@angular/material/list';
import {NgForOf} from '@angular/common';
import {MatIconButton} from '@angular/material/button';
import {MatTooltip} from '@angular/material/tooltip';
import {MatIcon} from '@angular/material/icon';
import {adminRouting} from '../../modules/admin-module/admin.routing';
export interface NavItem {
path: string; // absolute path like /dashboard
title: string; // label to show
icon?: string; // material icon name
exact?: boolean; // whether to match exactly
}
@Component({
selector: 'app-vertical-nav',
imports: [
MatTooltip,
MatIcon,
MatListItemIcon,
MatListItemTitle,
RouterLinkActive,
RouterLink,
MatListItem,
MatNavList,
NgForOf,
MatIconButton,
],
templateUrl: './vertical-nav.component.html',
styleUrl: './vertical-nav.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class VerticalNavComponent implements OnInit{
private router = inject(Router);
// Optional: if you want to pass items directly instead of deriving from routes
@Input() items: NavItem[] | null = null;
// Optional: pass a specific Route[] (e.g., ADMIN_ROUTES). If not provided, uses router.config.
@Input() routes: Route[] = [];
// Optional: prepend a base path (e.g., '/admin') when using feature routes.
@Input() basePath = '';
// Sidebar state (expanded/collapsed)
opened = signal(true);
navItems = signal<NavItem[]>([]);
readonly hasItems = computed(() => (this.items?.length ?? this.navItems().length) > 0);
@Output() collapsedChange = new EventEmitter<boolean>();
constructor() {
}
ngOnInit(): void {
this.navItems.set(this.deriveFromRoutes(this.routes, this.basePath));
}
toggle(): void {
this.opened.update(v => !v);
this.collapsedChange.emit(!this.opened());
}
private deriveFromRoutes(routes: Route[], base: string): NavItem[] {
// Flatten one level; you can make this recursive if needed
const items: NavItem[] = [];
for (const r of routes) {
if (!r || r.path === '**') continue;
const data = (r as any).data || {};
const seg = r.path || '';
const nextBase = [base, seg].filter(Boolean).join('/');
/* // Recurse into children if present (common in feature modules)
if (r.children && r.children.length) {
items.push(...this.deriveFromRoutes(r.children, nextBase));
}
*/
if (data.nav === false) continue;
const title = data.title as string | undefined;
if (!title) continue;
const icon = data.icon as string | undefined;
const full = '/' + nextBase.replace(/\/+/g, '/');
items.push({ path: full, title, icon, exact: true });
}
return items;
}
}
@@ -0,0 +1,7 @@
import { Table } from './table';
describe('Table', () => {
it('should create an instance', () => {
expect(new Table()).toBeTruthy();
});
});
+4
View File
@@ -0,0 +1,4 @@
export class Table {
schema?: string;
table?: string;
}
@@ -1,8 +1,10 @@
import { Injectable } from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
import {HttpClient, HttpParams} from '@angular/common/http';
import {User} from '../../models/user.model';
import {GlobalConstants} from '../../global-constants';
import {Table} from '../../models/table';
import {Role} from '../../models/role';
@Injectable({
providedIn: 'root'
@@ -13,8 +15,26 @@ export class AdminModuleService {
constructor(private http: HttpClient) {
}
updateUserRoles(fileName: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${fileName}`, data);
getUsers(): Observable<User[]> {
return this.http.get<User[]>(`${this.baseUrl}/users`);
}
getAllRoles(): Observable<Role[]> {
return this.http.get<Role[]>(`${this.baseUrl}/roles`);
}
updateUserRoles(id: any,data: User): Observable<any> {
return this.http.put(`${this.baseUrl}/users/${id}`, data);
}
/*
getUserTables(schema?: string): Observable<Table[]> {
let params = new HttpParams();
if (schema) {
params = params.set('schema', schema);
}
return this.http.get<Table[]>(`${this.baseUrl}/admin/json-dump/get-tables`, { params });
}
*/
}
@@ -1,11 +1,16 @@
<app-side-bar-admin
class="pc-sidebar"
>
</app-side-bar-admin>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet />
<app-vertical-nav
[routes]="adminRoutes"
[basePath]="basePath"
(collapsedChange)="navCollapsedChanged($event)">
</app-vertical-nav>
<div class="app-breadcrumb" [class.collapsed]="isCollapsed">
<app-breadcrumbs></app-breadcrumbs>
</div>
<div class="app-container" [class.collapsed]="isCollapsed">
<div class="page-component">
<router-outlet />
</div>
</div>
@@ -1,16 +1,36 @@
//---------------
.pc-sidebar{
top: 65px;
overflow-y: auto;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
}
.pc-container{
top: 0px;
.app-container{
top: -12px;
padding-left: 5px;
padding-right: 5px;
margin-left: 240px;
padding-top: 1px;
position: relative;
&.collapsed{
margin-left: 64px;
}
}
.app-breadcrumb{
position: fixed;
left: 240px;
width: 100%;
background-color: rgba(153, 153, 153, 0.16);
backdrop-filter: blur(8px);
z-index: 100;
&.collapsed{
left: 64px;
}
}
.page-component{
height: 100%;
margin-top: 65px;
margin-bottom: 50px;
margin-left: 10px;
margin-right: 10px;
background: aliceblue;
position: relative;
padding: 20px;
}
@@ -1,69 +1,31 @@
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 {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {RouterOutlet} from "@angular/router";
import {BreadcrumbsComponent} from '../../../components/breadcrumbs.component/breadcrumbs.component';
import {VerticalNavComponent} from '../../../components/vertical-nav.component/vertical-nav.component';
import {ADMIN_ROUTES} from '../admin.routing';
@Component({
selector: 'app-admin.component',
imports: [
RouterOutlet,
SideBarAdminComponent,
BreadcrumbsComponent
BreadcrumbsComponent,
VerticalNavComponent
],
schemas:[CUSTOM_ELEMENTS_SCHEMA],
templateUrl: './admin.component.html',
styleUrl: './admin.component.scss'
})
export class AdminComponent implements OnInit {
private document = inject<Document>(DOCUMENT);
height = 0;
export class AdminComponent{
constructor(private router: Router) {
this.height = 0
//adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']);
adminRoutes = (ADMIN_ROUTES[0]?.children ?? []).filter(r => r.path && r.data?.['nav']);
// If your admin module is mounted at '/admin' in the app routes, keep this as '/admin'.
// If it's at root, set this to ''.
basePath = '/main/admin';
isCollapsed = false;
navCollapsedChanged($event: boolean) {
this.isCollapsed = $event;
}
// public props
navCollapsed: boolean = false;
navCollapsedMob: boolean = false;
// public method
navMobClick() {
if (this.navCollapsedMob && !document.querySelector('app-navigation.pc-sidebar')?.classList.contains('mob-open')) {
this.navCollapsedMob = !this.navCollapsedMob;
setTimeout(() => {
this.navCollapsedMob = !this.navCollapsedMob;
}, 100);
} else {
this.navCollapsedMob = !this.navCollapsedMob;
}
if (document.querySelector('app-navigation.pc-sidebar')?.classList.contains('navbar-collapsed')) {
document.querySelector('app-navigation.pc-sidebar')?.classList.remove('navbar-collapsed');
}
}
handleKeyDown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
this.closeMenu();
}
}
closeMenu() {
if (document.querySelector('app-navigation.pc-sidebar')?.classList.contains('mob-open')) {
document.querySelector('app-navigation.pc-sidebar')?.classList.remove('mob-open');
}
}
ngOnInit() {
if(this.document.defaultView !== null || this.document.defaultView !== undefined) {
// @ts-ignore
this.height = this.document.defaultView.innerHeight;
}
else
this.height = 0;
}
}
@@ -1,32 +1,37 @@
import {RouterModule, Routes} from '@angular/router';
import {AdminComponent} from './admin.component/admin.component';
const ADMIN_ROUTES: Routes = [
export const ADMIN_ROUTES: Routes = [
{
path: '',
component: AdminComponent,
data:{breadcrumb: 'Admin'},
loadComponent: () => import('../admin-module/admin.component/admin.component').then((c) => c.AdminComponent),
data:{title: 'Admin', icon:'', nav:true},
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'}
data:{title: 'Welcome', icon:'dashboard', nav:true}
},
{
path: 'settings',
loadComponent: () => import('../admin-module/settings.component/settings.component').then((c) => c.SettingsComponent),
data:{breadcrumb: 'Settings'}
data:{title: 'Settings', icon:'settings', nav:true}
},
{
path: 'system',
loadComponent: () => import('../admin-module/system.component/system.component').then((c) => c.SystemComponent),
data:{breadcrumb: 'System'}
data:{title: 'System', icon:'component_exchange', nav:true}
},
{
path: 'users',
loadComponent: () => import('../admin-module/users.component/users.component').then((c) => c.UsersComponent),
data:{breadcrumb: 'Users'}
data:{title: 'Users', icon:'', nav:true}
},
{
path:"data-base",
loadComponent: () => import('../admin-module/data-base.component/data-base.component').then((c) => c.DataBaseComponent),
data:{title: 'Data', icon:'data_table', nav:true}
}
]
}
@@ -0,0 +1,59 @@
<div class="json-dump-container" *ngIf="!loading; else loadingTpl">
<h2>JSON Dump Management</h2>
<!-- Entity-specific Export/Import -->
<div class="entity-actions" *ngFor="let entity of entityTypes">
<h3>{{ entity | titlecase }}</h3>
<button mat-raised-button color="primary" (click)="exportEntity(entity)">
<mat-icon>download</mat-icon> Export {{ entity }}
</button>
<label mat-raised-button color="accent">
<mat-icon>upload</mat-icon> Import {{ entity }}
<input type="file" hidden accept=".json" (change)="onFileSelected($event, entity)">
</label>
</div>
<!-- Files Table -->
<h3>Available JSON Files</h3>
<mat-table [dataSource]="files">
<ng-container matColumnDef="fileName">
<mat-header-cell *matHeaderCellDef> File Name </mat-header-cell>
<mat-cell *matCellDef="let file">{{ file.fileName }}</mat-cell>
</ng-container>
<ng-container matColumnDef="size">
<mat-header-cell *matHeaderCellDef> Size </mat-header-cell>
<mat-cell *matCellDef="let file">{{ jsonDumpService.formatFileSize(file.size) }}</mat-cell>
</ng-container>
<ng-container matColumnDef="lastModified">
<mat-header-cell *matHeaderCellDef> Last Modified </mat-header-cell>
<mat-cell *matCellDef="let file">{{ jsonDumpService.formatDate(file.lastModified) }}</mat-cell>
</ng-container>
<ng-container matColumnDef="actions">
<mat-header-cell *matHeaderCellDef> Actions </mat-header-cell>
<mat-cell *matCellDef="let file">
<button mat-icon-button (click)="downloadFile(file.fileName)" matTooltip="Download">
<mat-icon>download</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteFile(file.fileName)" matTooltip="Delete">
<mat-icon>delete</mat-icon>
</button>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="['fileName', 'size', 'lastModified', 'actions']"></mat-header-row>
<mat-row *matRowDef="let row; columns: ['fileName', 'size', 'lastModified', 'actions']"></mat-row>
</mat-table>
<!-- Archives -->
<h3>Archives</h3>
<mat-list>
<mat-list-item *ngFor="let archive of archives">{{ archive }}</mat-list-item>
</mat-list>
</div>
<ng-template #loadingTpl>
<mat-spinner></mat-spinner>
</ng-template>
@@ -0,0 +1,27 @@
.json-dump-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 16px;
}
.export-buttons,
.import-section {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.actions {
margin-bottom: 16px;
button {
margin-right: 8px;
}
}
table {
width: 100%;
}
mat-table {
margin-bottom: 24px;
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { JsonDumpComponent } from './json-dump.component';
describe('JsonDumpComponent', () => {
let component: JsonDumpComponent;
let fixture: ComponentFixture<JsonDumpComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [JsonDumpComponent]
})
.compileComponents();
fixture = TestBed.createComponent(JsonDumpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,135 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA, OnInit} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import {MatList, MatListItem} from '@angular/material/list';
import {
MatCell, MatCellDef,
MatColumnDef,
MatHeaderCell, MatHeaderCellDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef, MatTable
} from '@angular/material/table';
import {MatIconButton} from '@angular/material/button';
import {NgForOf, NgIf, TitleCasePipe} from '@angular/common';
import {MatProgressSpinner} from '@angular/material/progress-spinner';
import {MatIcon} from '@angular/material/icon';
import {MatTooltip} from '@angular/material/tooltip';
import {ArchivesResponse, FileInfo, FilesResponse, JsonDumpService} from '../../json-dump.service';
@Component({
selector: 'app-json-dump',
templateUrl: './json-dump.component.html',
imports: [
MatProgressSpinner,
MatListItem,
MatList,
MatHeaderRow,
MatRow,
MatHeaderRowDef,
MatRowDef,
MatIcon,
MatHeaderCell,
MatCell,
MatIconButton,
MatTooltip,
MatColumnDef,
MatHeaderCellDef,
MatCellDef,
MatTable,
TitleCasePipe,
NgIf,
NgForOf
],
styleUrls: ['./json-dump.component.scss'],
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class JsonDumpComponent implements OnInit {
files: FileInfo[] = [];
archives: string[] = [];
loading = false;
entityTypes = ['users', 'roles', 'tutorials'];
constructor(
public jsonDumpService: JsonDumpService,
private snackBar: MatSnackBar
) {}
ngOnInit(): void {
this.loadFiles();
this.loadArchives();
}
loadFiles(): void {
this.loading = true;
this.jsonDumpService.getFiles().subscribe({
next: (res: FilesResponse) => {
this.files = res.files || [];
this.loading = false;
},
error: (err: { message: any; }) => {
this.loading = false;
this.showMessage(`Error loading files: ${err.message}`);
}
});
}
loadArchives(): void {
this.jsonDumpService.getArchivedFiles().subscribe({
next: (res: ArchivesResponse) => {
this.archives = res.archives || [];
},
error: (err: { message: any; }) => {
this.showMessage(`Error loading archives: ${err.message}`);
}
});
}
exportEntity(entity: string): void {
this.jsonDumpService.exportEntity(entity).subscribe({
next: () => {
this.showMessage(`Exported ${entity} successfully`);
this.loadFiles();
},
error: (err: { message: any; }) => this.showMessage(`Export ${entity} failed: ${err.message}`)
});
}
importEntity(entity: string, file: File): void {
this.jsonDumpService.importEntity(entity, file).subscribe({
next: () => this.showMessage(`Imported ${entity} successfully`),
error: (err: { message: any; }) => this.showMessage(`Import ${entity} failed: ${err.message}`)
});
}
onFileSelected(event: Event, entity: string): void {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.importEntity(entity, input.files[0]);
input.value = ''; // reset input
}
}
deleteFile(fileName: string): void {
this.jsonDumpService.deleteFile(fileName).subscribe({
next: () => {
this.showMessage(`Deleted file: ${fileName}`);
this.loadFiles();
},
error: (err: { message: any; }) => this.showMessage(`Delete failed: ${err.message}`)
});
}
downloadFile(fileName: string): void {
this.jsonDumpService.downloadFile(fileName).subscribe({
next: (blob: any) => this.jsonDumpService.downloadBlob(blob, fileName),
error: (err: { message: any; }) => this.showMessage(`Download failed: ${err.message}`)
});
}
private showMessage(msg: string): void {
this.snackBar.open(msg, 'Close', { duration: 3000 });
}
}
@@ -0,0 +1,7 @@
<p>data-base.component works!</p>
<ul>
@for (table of tables; track table){
<li>{{ table.schema }}.{{ table.table }}</li>
}
</ul>
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DataBaseComponent } from './data-base.component';
describe('DataBaseComponent', () => {
let component: DataBaseComponent;
let fixture: ComponentFixture<DataBaseComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DataBaseComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DataBaseComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,25 @@
import { Component } from '@angular/core';
import {Table} from '../../../models/table';
import {AdminModuleService} from '../admin-module.service';
@Component({
selector: 'app-data-base.component',
imports: [],
templateUrl: './data-base.component.html',
styleUrl: './data-base.component.scss'
})
export class DataBaseComponent {
tables: Table[] = [];
constructor(private adminService:AdminModuleService) {
this.getTables();
}
getTables(){
// this.adminService.getUserTables(/* optionally: 'public' */).subscribe({
// next: data => (this.tables = data),
// error: err => console.error('Failed to load tables', err)
// });
}
}
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { JsonDumpService } from './json-dump.service';
describe('JsonDumpService', () => {
let service: JsonDumpService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(JsonDumpService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
@@ -0,0 +1,449 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import {GlobalConstants} from '../../global-constants';
// Interfaces
export interface ApiResponse<T = any> {
success: boolean;
message: string;
timestamp?: string;
data?: T;
}
export interface ExportResponse extends ApiResponse {
timestamp: string;
}
export interface ImportResponse extends ApiResponse {
timestamp: string;
fileName?: string;
}
export interface FileInfo {
fileName: string;
size: number;
lastModified: string;
//path: string;
}
export interface FilesResponse extends ApiResponse {
files: FileInfo[];
count: number;
}
export interface ArchivesResponse extends ApiResponse {
archives: string[];
count: number;
}
export interface ExtractResponse extends ApiResponse {
fileName: string;
targetDirectory: string;
}
@Injectable({
providedIn: 'root'
})
export class JsonDumpService {
private readonly baseUrl = `${GlobalConstants.API_URL}/admin/json-dump`;
constructor(private http: HttpClient) {}
exportEntity(entity: string) {
return this.http.post<{message: string}>(`${this.baseUrl}/export/${entity}`, {});
}
importEntity(entity: string, file: File) {
const formData = new FormData();
formData.append('file', file);
return this.http.post(`${this.baseUrl}/import/${entity}`, formData);
}
// ==================== EXPORT METHODS ====================
/**
* Export all entities (users, tutorials, roles)
*/
exportAll(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/all`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export users only
*/
exportUsers(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/users`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export tutorials only
*/
exportTutorials(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/tutorials`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Export roles only
*/
exportRoles(): Observable<ExportResponse> {
return this.http.post<ExportResponse>(`${this.baseUrl}/export/roles`, {})
.pipe(
catchError(this.handleError)
);
}
// ==================== IMPORT METHODS ====================
/**
* Import users from latest JSON file
*/
importUsers(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/users`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import tutorials from latest JSON file
*/
importTutorials(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/tutorials`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import roles from latest JSON file
*/
importRoles(): Observable<ImportResponse> {
return this.http.post<ImportResponse>(`${this.baseUrl}/import/roles`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Import users from uploaded file
*/
importUsersFromFile(file: File): Observable<ImportResponse> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<ImportResponse>(`${this.baseUrl}/import/users/file`, formData)
.pipe(
catchError(this.handleError)
);
}
/**
* Import tutorials from uploaded file
*/
importTutorialsFromFile(file: File): Observable<ImportResponse> {
const formData = new FormData();
formData.append('file', file);
return this.http.post<ImportResponse>(`${this.baseUrl}/import/tutorials/file`, formData)
.pipe(
catchError(this.handleError)
);
}
// ==================== ARCHIVE METHODS ====================
/**
* Get list of all archived files
*/
getArchivedFiles(): Observable<ArchivesResponse> {
return this.http.get<ArchivesResponse>(`${this.baseUrl}/archives`)
.pipe(
catchError(this.handleError)
);
}
archiveOldFiles(): Observable<ApiResponse> {
return this.http.post<ApiResponse>(`${this.baseUrl}/archives/archive-old-files`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Extract archived file
*/
extractArchive(fileName: string): Observable<ExtractResponse> {
return this.http.post<ExtractResponse>(`${this.baseUrl}/archives/${fileName}/extract`, {})
.pipe(
catchError(this.handleError)
);
}
/**
* Download archived file
*/
downloadArchive(fileName: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/archives/${fileName}/download`, {
responseType: 'blob'
}).pipe(
catchError(this.handleError)
);
}
// ==================== FILE MANAGEMENT METHODS ====================
/**
* Get list of all JSON files with details
*/
getFiles(): Observable<FilesResponse> {
return this.http.get<FilesResponse>(`${this.baseUrl}/files`)
.pipe(
catchError(this.handleError)
);
}
/**
* Download JSON file
*/
downloadFile(fileName: string): Observable<Blob> {
return this.http.get(`${this.baseUrl}/files/${fileName}/download`, {
responseType: 'blob'
}).pipe(
catchError(this.handleError)
);
}
/**
* Delete JSON file
*/
deleteFile(fileName: string): Observable<ApiResponse> {
return this.http.delete<ApiResponse>(`${this.baseUrl}/files/${fileName}`)
.pipe(
catchError(this.handleError)
);
}
// ==================== UTILITY METHODS ====================
/**
* Download blob as file
*/
downloadBlob(blob: Blob, fileName: string): void {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
/**
* Format file size for display
*/
formatFileSize(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Format date for display
*/
formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleString();
}
/**
* Validate JSON file
*/
isValidJsonFile(file: File): boolean {
return file.type === 'application/json' || file.name.toLowerCase().endsWith('.json');
}
/**
* Get entity type from file name
*/
getEntityTypeFromFileName(fileName: string): string {
if (fileName.includes('users_')) return 'users';
if (fileName.includes('tutorials_')) return 'tutorials';
if (fileName.includes('roles_')) return 'roles';
return 'unknown';
}
/**
* Check if file is recent (less than 7 days old)
*/
isRecentFile(lastModified: string): boolean {
const fileDate = new Date(lastModified);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
return fileDate > sevenDaysAgo;
}
// ==================== BATCH OPERATIONS ====================
/**
* Export all entities and return combined status
*/
// exportAllSeparately(): Observable<{ users: ExportResponse; tutorials: ExportResponse; roles: ExportResponse }> {
// const users$ = this.exportUsers();
// const tutorials$ = this.exportTutorials();
// const roles$ = this.exportRoles();
//
// return new Observable(observer => {
// const results: any = {};
// let completed = 0;
//
// const checkCompletion = () => {
// if (completed === 3) {
// observer.next(
// value:{users:users$,tutorial:tutorials$,roles:roles$},
// );
// observer.complete();
// }
// };
//
// users$.subscribe({
// next: (result) => {
// results.users = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.users = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
//
// tutorials$.subscribe({
// next: (result) => {
// results.tutorials = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.tutorials = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
//
// roles$.subscribe({
// next: (result) => {
// results.roles = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.roles = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
// });
// }
/**
* Import all entities from latest files
*/
// importAll(): Observable<{ users: ImportResponse; tutorials: ImportResponse; roles: ImportResponse }> {
// const users$ = this.importUsers();
// const tutorials$ = this.importTutorials();
// const roles$ = this.importRoles();
//
// return new Observable(observer => {
// const results: any = {};
// let completed = 0;
//
// const checkCompletion = () => {
// if (completed === 3) {
// observer.next(
// value:{user:}
// );
// observer.complete();
// }
// };
//
// users$.subscribe({
// next: (result) => {
// results.users = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.users = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
//
// tutorials$.subscribe({
// next: (result) => {
// results.tutorials = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.tutorials = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
//
// roles$.subscribe({
// next: (result) => {
// results.roles = result;
// completed++;
// checkCompletion();
// },
// error: (error) => {
// results.roles = { success: false, message: error.message, timestamp: new Date().toISOString() };
// completed++;
// checkCompletion();
// }
// });
// });
// }
// ==================== ERROR HANDLING ====================
private handleError = (error: HttpErrorResponse): Observable<never> => {
let errorMessage = 'An unknown error occurred';
if (error.error instanceof ErrorEvent) {
// Client-side error
errorMessage = error.error.message;
} else {
// Server-side error
if (error.error && error.error.message) {
errorMessage = error.error.message;
} else if (error.message) {
errorMessage = error.message;
} else {
errorMessage = `Server returned code ${error.status}: ${error.statusText}`;
}
}
console.error('JsonDumpService Error:', errorMessage, error);
return throwError(() => new Error(errorMessage));
};
}
@@ -25,3 +25,5 @@
</mat-tab-group>
</mat-card-content>
</mat-card>
<mat-divider></mat-divider>
<app-json-dump></app-json-dump>
@@ -13,6 +13,7 @@ import {
} from '@angular/material/table';
import {SystemService} from '../../../services/system.service';
import {NameValueItem} from '../../../models/name-value-item';
import {JsonDumpComponent} from '../components/json-dump.component/json-dump.component';
@Component({
selector: 'app-settings.component',
@@ -32,7 +33,8 @@ import {NameValueItem} from '../../../models/name-value-item';
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef
MatRowDef,
JsonDumpComponent
],
templateUrl: './settings.component.html',
styleUrl: './settings.component.scss'
@@ -1,35 +0,0 @@
<mat-nav-list>
<a mat-list-item routerLink="admin-welcome">
<span class="entry">
<mat-icon>house</mat-icon>
@if (!isCollapsed) {
<span >Dashboard</span>
}
</span>
</a>
<a mat-list-item routerLink="users">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Users</span>
}
</span>
</a>
<a mat-list-item routerLink="settings">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >Settings</span>
}
</span>
</a>
<a mat-list-item routerLink="system">
<span class="entry">
<mat-icon>newspaper</mat-icon>
@if (!isCollapsed) {
<span >System</span>
}
</span>
</a>
</mat-nav-list>
@@ -1,14 +0,0 @@
.entry{
display: flex;
align-items: center;
gap: 1rem;
padding:0.75rem;
color: rgba(24, 255, 255, 0.96);
}
a.mdc-list-item
{
background-color: rgba(24,255,255,0.04);
}
@@ -1,20 +0,0 @@
import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
import {MatListItem, MatNavList} from '@angular/material/list';
import {RouterLink} from '@angular/router';
import {MatIcon} from '@angular/material/icon';
@Component({
selector: 'app-side-bar-admin',
imports: [
MatIcon,
MatListItem,
MatNavList,
RouterLink
],
templateUrl: './side-bar-admin.component.html',
styleUrl: './side-bar-admin.component.scss',
schemas:[CUSTOM_ELEMENTS_SCHEMA]
})
export class SideBarAdminComponent {
isCollapsed = false;
}
@@ -53,15 +53,15 @@ export class UsersComponent {
displayedColumns: string[] = UserColumns.map((col) => col.key)
columnsSchema: any = UserColumns
constructor(private userService: UserService,
private roleService: RolesService,
constructor(
private adminService:AdminModuleService) {
this.getAllRoles();
this.getUsers();
}
getAllRoles():any{
this.roleService.getAllRoles().subscribe(
this.adminService.getAllRoles().subscribe(
(data : any) => {
this.allRoles = data;
console.log(data);
@@ -73,7 +73,7 @@ export class UsersComponent {
}
getUsers(){
this.userService.getAdminBoard().subscribe(
this.adminService.getUsers().subscribe(
(data : any) => {
this.users = data;
console.log(data);
@@ -84,7 +84,7 @@ export class UsersComponent {
);
}
saveUserRoles(user:User){
saveUserRoles(user:User){
this.adminService.updateUserRoles(user.id, user).subscribe(
(data:any) => {
console.log(data);
@@ -114,6 +114,8 @@ export class UsersComponent {
isDisabled(element:User, role: Role) {
return role.name?.includes(<string>element.username?.toUpperCase());
}
}
export const UserColumns = [
@@ -1,60 +1,63 @@
<mat-toolbar class="fixed-top">
<button mat-button routerLink="">
{{title}}
</button>
<button mat-raised-button routerLink="./ai/generate-image">
Generate Image
</button>
<button mat-raised-button routerLink="./tutorials/tutorials-all">
Tutorials
</button>
<span class="menu-spacer"></span>
@if (!isLoggedIn) {
<button matMiniFab matTooltip="Sign Up" aria-label="Sign Up" (click)="openSignupDialog('100ms', '5ms')">
<mat-icon>person_add</mat-icon>
<!-- <mat-toolbar-row>-->
<button mat-button routerLink="">
{{title}}
</button>
<button mat-raised-button routerLink="./ai/generate-image">
Generate Image
</button>
<button mat-raised-button routerLink="./tutorials/tutorials-all">
Tutorials
</button>
<button matMiniFab matTooltip="Login" aria-label="Login" (click)="openLoginDialog('100ms', '5ms')" >
<mat-icon>login</mat-icon>
</button>
}@else {
<button matButton="elevated" aria-label="Account" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>
<mat-label>{{username}}</mat-label>
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
}
@if (showUserBoard) {
<button mat-menu-item routerLink="user/user-welcome">
<mat-icon>space_dashboard</mat-icon>
User tools
</button>
}
@if (showModeratorBoard) {
<button mat-menu-item routerLink="moderator/moderator-welcome">
<mat-icon>space_dashboard</mat-icon>
Moderator board
</button>
}
<span class="menu-spacer"></span>
<button mat-menu-item routerLink="profile">
<mat-icon>person</mat-icon>
Profile
@if (!isLoggedIn) {
<button matMiniFab matTooltip="Sign Up" aria-label="Sign Up" (click)="openSignupDialog('100ms', '5ms')">
<mat-icon>person_add</mat-icon>
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
<button matMiniFab matTooltip="Login" aria-label="Login" (click)="openLoginDialog('100ms', '5ms')" >
<mat-icon>login</mat-icon>
</button>
</mat-menu>
}
}@else {
<button matButton="elevated" aria-label="Account" [matMenuTriggerFor]="menu">
<mat-icon>account_circle</mat-icon>
<mat-label>{{username}}</mat-label>
</button>
<mat-menu #menu="matMenu">
@if (showAdminBoard) {
<button mat-menu-item routerLink="admin/admin-welcome">
<mat-icon>admin_panel_settings</mat-icon>
Admin Panel
</button>
}
@if (showUserBoard) {
<button mat-menu-item routerLink="user/user-welcome">
<mat-icon>space_dashboard</mat-icon>
User tools
</button>
}
@if (showModeratorBoard) {
<button mat-menu-item routerLink="moderator/moderator-welcome">
<mat-icon>space_dashboard</mat-icon>
Moderator board
</button>
}
<button mat-menu-item routerLink="profile">
<mat-icon>person</mat-icon>
Profile
</button>
<button mat-menu-item (click)="logout()">
<mat-icon>logout</mat-icon>
Logout
</button>
</mat-menu>
}
<!-- </mat-toolbar-row>-->
</mat-toolbar>
@@ -30,6 +30,7 @@ mat-toolbar{
margin-top: 65px;
height: 100%;
min-height: fit-content;
width: 100%;
}
.component{
@@ -1,10 +1,10 @@
import {RouterModule, Routes} from '@angular/router';
import {MainComponent} from './main.component/main.component';
const MAIN_ROUTES: Routes =[
export const MAIN_ROUTES: Routes =[
{
path: '',
component: MainComponent,
loadComponent: () => import('./main.component/main.component').then((c) => c.MainComponent),
children: [
{
path: 'tutorials',
@@ -21,6 +21,7 @@ const MAIN_ROUTES: Routes =[
path: 'admin',
loadChildren: () =>
import('../admin-module/admin.module').then((m) => m.AdminModule),
data:{nav:true}
},
{
path: 'user',
@@ -1,5 +1,2 @@
<app-side-bar-moderator class="pc-sidebar" ></app-side-bar-moderator>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet></router-outlet>
</div>
<app-inner-layout [moduleRoutes]="moderatorRouts" [basePath]="basePath"></app-inner-layout>
@@ -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;
}
@@ -2,20 +2,21 @@ 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';
import {InnerLayoutComponent} from '../../../components/inner-layout.component/inner-layout.component';
import {MODERATOR_ROUTS} from '../moderator.routing';
@Component({
selector: 'app-moderator.component',
imports: [
RouterOutlet,
SideBarModeratorComponent,
BreadcrumbsComponent,
InnerLayoutComponent,
],
templateUrl: './moderator.component.html',
styleUrl: './moderator.component.scss'
})
export class ModeratorComponent {
moderatorRouts = MODERATOR_ROUTS;
basePath = '/main/moderator';
}
@@ -2,30 +2,30 @@ import {RouterModule, Routes} from '@angular/router';
import {ModeratorComponent} from './moderator.component/moderator.component';
const MODERATOR_ROUTES: Routes = [
export const MODERATOR_ROUTS: Routes = [
{
path: '',
component: ModeratorComponent,
data:{breadcrumb: 'Moderator'},
data:{title: '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'}
data:{title: 'Welcome', icon:'dashboard', nav:true}
},
{
path: 'tutorials-list',
loadComponent: () => import('../moderator-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent),
data:{breadcrumb: 'Tutorials List'}
data:{title: 'Tutorials List', icon:'list', nav:true}
},
{
path: 'tutorial-preview',
loadComponent: () => import('../moderator-module/tutorial-preview.component/tutorial-preview.component').then((c) => c.TutorialPreviewComponent),
data:{breadcrumb: 'Tutorial Preview'}
data:{title: 'Tutorial Preview'}
}
]
}
];
export const moderatorRouting = RouterModule.forChild(MODERATOR_ROUTES);
export const moderatorRouting = RouterModule.forChild(MODERATOR_ROUTS);
@@ -1,10 +1,2 @@
<app-side-bar-user
class="pc-sidebar"
>
</app-side-bar-user>
<div class="pc-container">
<app-breadcrumbs></app-breadcrumbs>
<router-outlet></router-outlet>
</div>
<app-inner-layout [moduleRoutes]="userRouts" [basePath]="basePath"></app-inner-layout>
@@ -4,43 +4,24 @@ 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';
import {InnerLayoutComponent} from '../../../components/inner-layout.component/inner-layout.component';
import {MODERATOR_ROUTS} from '../../moderator-module/moderator.routing';
import {USER_ROUTS} from '../user.routing';
@Component({
selector: 'app-user.component',
imports: [
RouterOutlet,
SideBarUserComponent,
BreadcrumbsComponent
BreadcrumbsComponent,
InnerLayoutComponent
],
templateUrl: './user.component.html',
styleUrl: './user.component.scss',
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class UserComponent implements OnInit {
@ViewChild(MatSidenav)
sidenav!: MatSidenav;
isMobile= true;
isCollapsed = true;
export class UserComponent {
constructor(private observer: BreakpointObserver) {
}
ngOnInit(): void {
this.observer.observe(['(max-width: 800px)']).subscribe((screenSize) => {
this.isMobile = screenSize.matches;
})
}
toggleMenu() {
if(this.isMobile){
this.sidenav.toggle();
this.isCollapsed = false;
} else {
this.sidenav.open();
this.isCollapsed = !this.isCollapsed;
}
}
userRouts = USER_ROUTS;
basePath = '/main/user';
}
@@ -2,47 +2,47 @@ import {RouterModule, Routes} from '@angular/router';
import {UserComponent} from './user.component/user.component';
const USER_ROUTES: Routes = [
export const USER_ROUTS: Routes = [
{
path: '',
component: UserComponent,
data:{breadcrumb: 'User'},
data:{title: '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'}
data:{title: 'Welcome', icon:'dashboard', nav:true}
},
{
path: 'tutorials-list',
loadComponent: () => import('../user-module/tutorials-list.component/tutorials-list.component').then((c) => c.TutorialsListComponent),
data:{breadcrumb: 'Tutorials List'}
data:{title: 'Tutorials List', icon:'list', nav:true}
},
{
path: 'tutorial-add',
loadComponent: () => import('../user-module/tutorial-add.component/tutorial-add.component').then((c) => c.TutorialAddComponent),
data:{breadcrumb: 'Add Tutorial'}
data:{title: 'Add Tutorial'}
},
{
path: 'tutorial-edit',
loadComponent: () => import('../user-module/tutorial-edit.component/tutorial-edit.component').then((c) => c.TutorialEditComponent),
data:{breadcrumb: 'Edit Tutorial'}
data:{title: 'Edit Tutorial'}
},
{
path: 'ai-models',
loadComponent: () => import('../user-module/ai-models.component/ai-models.component').then((c) => c.AiModelsComponent),
data:{breadcrumb: 'AI'}
data:{title: 'AI'}
},
{
path: 'images',
loadComponent: () => import('../user-module/images.component/images.component').then((c) => c.ImagesComponent),
data:{breadcrumb: 'Images'}
data:{title: 'Images', icon: 'imagesmode', nav:true}
}
]
}
];
export const userRouting = RouterModule.forChild(USER_ROUTES);
export const userRouting = RouterModule.forChild(USER_ROUTS);
@@ -10,10 +10,10 @@ export class SpinnerService {
}
show() {
this.visibility.next(true);
this.visibility.next(false);
}
hide() {
this.visibility.next(false);
this.visibility.next(true);
}
}
@@ -24,8 +24,6 @@ export class UserService {
return this.http.get(API_URL + '/mod', { responseType: 'text' });
}
getAdminBoard(): Observable<User[]> {
return this.http.get<User[]>(API_URL);
}
}
+2 -2
View File
@@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">
<!--<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<!-- Google tag (gtag.js) -->
+2
View File
@@ -7,6 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<!--<link rel="stylesheet"
href="https://fonts.googleapis.com/css?family= Jersey 20 Charted:wght@300;400;500&display=swap">-->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body class="mat-typography">
+3 -3
View File
@@ -49,11 +49,11 @@ body {
}
.pc-container{
top: 0;
padding-left: 5px;
padding-right: 5px;
padding-left: 20px;
padding-right: 20px;
position: relative;
margin-left: 260px;
margin-left: 240px;
min-height: calc(100vh - 113px);
background: #fafafbb3;
}