diff --git a/package-lock.json b/package-lock.json index 805dc9d7..3fdfd55c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@nebular/theme": "^11.0.0", "@ngx-translate/core": "^14.0.0", "@ngx-translate/http-loader": "^7.0.0", - "@stripe/stripe-js": "^4.9.0", + "@stripe/stripe-js": "^4.10.0", "@types/lodash": "^4.14.194", "ag-grid-angular": "^31.3.1", "ag-grid-community": "^31.3.1", @@ -5036,6 +5036,7 @@ "version": "4.10.0", "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-4.10.0.tgz", "integrity": "sha512-KrMOL+sH69htCIXCaZ4JluJ35bchuCCznyPyrbN8JXSGQfwBI1SuIEMZNwvy8L8ykj29t6sa5BAAiL7fNoLZ8A==", + "license": "MIT", "engines": { "node": ">=12.16" } diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/mock-data.constants.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/mock-data.constants.ts new file mode 100644 index 00000000..e6d2acc8 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/mock-data.constants.ts @@ -0,0 +1,10 @@ +import {TitleDetails, UserDetails} from './user-title.interface'; + +export const USERS: UserDetails[] = [ + {id: '123', name: 'John Doe', email: 'john.doe123@example.com'}, + {id: '124', name: 'Jane Smith', email: 'jane.smith124@example.com'}, +]; +export const TITLES: TitleDetails[] = [ + {id: '1', title: 'Contract.pdf'}, + {id: '2', title: 'Appointment.pdf'}, +]; diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title.interface.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title.interface.ts new file mode 100644 index 00000000..683c23c1 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title.interface.ts @@ -0,0 +1,9 @@ +export interface UserDetails { + id: string; + name: string; + email: string; +} +export interface TitleDetails { + id: string; + title: string; +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.html b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.html new file mode 100644 index 00000000..0740b3a3 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.html @@ -0,0 +1,5 @@ +
+

Documentation

+

ID:{{ title?.id }}

+

Title:{{ title?.title }}

+
diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.ts new file mode 100644 index 00000000..b3165168 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component.ts @@ -0,0 +1,23 @@ +import {Component} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; +import {TitleDetails} from '../user-title.interface'; +import {TitleService} from './user-title.service'; + +@Component({ + selector: 'lib-user-title', + templateUrl: './user-title.component.html', +}) +export class UserTitleComponent { + title: TitleDetails; + constructor( + private readonly route: ActivatedRoute, + private readonly titleService: TitleService, + ) { + const id = this.route.snapshot.paramMap.get('id'); + if (id) { + this.titleService.getTitleById(id).subscribe(title => { + this.title = title; + }); + } + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service.ts new file mode 100644 index 00000000..e2e8f717 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service.ts @@ -0,0 +1,22 @@ +import {catchError, delay, map, Observable, of} from 'rxjs'; +import {TITLES} from '../mock-data.constants'; +import {TitleDetails} from '../user-title.interface'; + +export class TitleService { + private readonly titles = TITLES; + + getTitleById(id: string): Observable { + const title = this.titles.find(u => u.id === id); + return of(title); + } + getTitleNameForBreadcrumb( + params: Record, + ): Observable { + const id = params['id']; + return this.getTitleById(id).pipe( + map(titles => titles?.title || `Document #${id}`), + catchError(() => of(`Document #${id}`)), + delay(4000), // Simulating network delay + ); + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.html b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.html new file mode 100644 index 00000000..e2b69192 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.html @@ -0,0 +1,8 @@ +
+

User Details

+

ID: {{ user?.id }}

+

Name: {{ user?.name }}

+

Email: {{ user?.email }}

+ + +
diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.ts new file mode 100644 index 00000000..0b6f83ac --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.component.ts @@ -0,0 +1,27 @@ +import {CommonModule} from '@angular/common'; +import {Component} from '@angular/core'; +import {ActivatedRoute, RouterModule} from '@angular/router'; +import {UserDetails} from '../user-title.interface'; +import {UserService} from './user.service'; + +@Component({ + selector: 'lib-user', + standalone: true, + templateUrl: './user.component.html', + imports: [CommonModule, RouterModule], +}) +export class UserComponent { + user: UserDetails; + + constructor( + private readonly route: ActivatedRoute, + private readonly userService: UserService, + ) { + const id = this.route.snapshot.paramMap.get('id'); + if (id) { + this.userService.getUserById(id).subscribe(user => { + this.user = user; + }); + } + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.service.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.service.ts new file mode 100644 index 00000000..15ea3176 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb-demo/user/user.service.ts @@ -0,0 +1,20 @@ +import {catchError, delay, map, Observable, of} from 'rxjs'; +import {USERS} from '../mock-data.constants'; +import {UserDetails} from '../user-title.interface'; + +export class UserService { + private readonly users = USERS; + + getUserById(id: string): Observable { + const user = this.users.find(u => u.id === id); + return of(user); + } + getUserNameForBreadcrumb(params: Record): Observable { + const id = params['id']; // Access any param key dynamically + return this.getUserById(id).pipe( + map(user => user?.name || `User #${id}`), + catchError(() => of(`User #${id}`)), + delay(4000), + ); + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.html b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.html new file mode 100644 index 00000000..06018cba --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.html @@ -0,0 +1,121 @@ + diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.scss b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.scss new file mode 100644 index 00000000..31c73db6 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.scss @@ -0,0 +1,86 @@ +.breadcrumb { + display: flex; + flex-wrap: wrap; + list-style: none; + padding: 0; + margin: 0; + background-color: transparent; + + .breadcrumb-item { + display: flex; + align-items: center; + font-size: 0.875rem; + color: #555; + + a { + color: #007bff; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + &.active { + font-weight: 600; + color: #333; + } + + &.clickable { + cursor: pointer; + color: #007bff; + text-decoration: underline; + + &:hover { + color: #0056b3; + } + } + + .breadcrumb-label { + max-width: 12rem; // Restrict label width + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: inline-block; + vertical-align: middle; + } + } + + .separator { + margin: 0 0.375rem; + color: #aaa; + font-size: 0.875rem; + } +} + +.breadcrumb-skeleton { + display: flex; + align-items: center; + padding: 0.5rem 0; + + .skeleton-item { + height: 1rem; + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: 4px; + min-width: 3.75rem; + max-width: 7.5rem; + width: 5rem; + } + + .separator { + margin: 0 0.375rem; + color: #ccc; + font-size: 0.875rem; + } +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.ts new file mode 100644 index 00000000..4c1ed979 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.component.ts @@ -0,0 +1,50 @@ +import {Component, Input, isDevMode, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Breadcrumb} from './breadcrumb.interface'; +import {BreadcrumbService} from './breadcrumb.service'; +import {Observable, of, Subject, takeUntil} from 'rxjs'; +import {RouterModule} from '@angular/router'; +import {NbIconModule} from '@nebular/theme'; +@Component({ + selector: 'app-breadcrumb', + templateUrl: './breadcrumb.component.html', + standalone: true, + imports: [CommonModule, RouterModule, NbIconModule], + styleUrls: ['./breadcrumb.component.scss'], +}) +export class BreadcrumbComponent implements OnInit { + breadcrumbs$: Observable = this.breadcrumbService.breadcrumbs; + loading$: Observable = this.breadcrumbService.loading; + + @Input() staticBreadcrumbs = []; + @Input() separator = '>'; + @Input() maxItems = 8; + @Input() separatorClass = 'separator'; + @Input() itemClass = 'breadcrumb-item'; + @Input() maxLabelLength = 30; + @Input() showLoadingSkeleton = true; + @Input() showIcons = false; + + expanded = false; + private destroy$ = new Subject(); + constructor(private readonly breadcrumbService: BreadcrumbService) {} + ngOnInit(): void { + this.breadcrumbs$.pipe(takeUntil(this.destroy$)).subscribe(breadcrumbs => { + if (isDevMode()) { + console.log('Breadcrumbs:', breadcrumbs); + } + }); + } + getTrimmedLabel(label: string): string { + return label.length > this.maxLabelLength + ? label.slice(0, this.maxLabelLength).trimEnd() + '…' + : label; + } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + } + toggleExpand() { + this.expanded = !this.expanded; + } +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.interface.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.interface.ts new file mode 100644 index 00000000..69bdc099 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.interface.ts @@ -0,0 +1,6 @@ +export interface Breadcrumb { + label: string; + url: string; + skipLink?: boolean; + icon?: string; +} diff --git a/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.service.ts b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.service.ts new file mode 100644 index 00000000..26d21486 --- /dev/null +++ b/projects/arc-lib/src/lib/components/breadcrumb/breadcrumb.service.ts @@ -0,0 +1,129 @@ +import {Injectable, Injector} from '@angular/core'; +import {ActivatedRouteSnapshot, NavigationEnd, Router} from '@angular/router'; +import {BehaviorSubject, EMPTY, Observable} from 'rxjs'; +import {catchError, filter, finalize, tap} from 'rxjs/operators'; +import {Breadcrumb} from './breadcrumb.interface'; + +@Injectable({providedIn: 'root'}) +export class BreadcrumbService { + private readonly breadcrumbs$ = new BehaviorSubject([]); + private readonly loading$ = new BehaviorSubject(false); + + constructor( + private readonly router: Router, + private readonly injector: Injector, + ) { + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(async () => { + const root = this.router.routerState.snapshot.root; + const breadcrumbs = this.buildBreadcrumbs(root); + this.breadcrumbs$.next(breadcrumbs); + }); + } + + private buildBreadcrumbs( + route: ActivatedRouteSnapshot, + url = '', + breadcrumbs: Breadcrumb[] = [], + ): Breadcrumb[] { + if (!route.routeConfig) { + return route.firstChild + ? this.buildBreadcrumbs(route.firstChild, url, breadcrumbs) + : breadcrumbs; + } + + let path = route.routeConfig.path || ''; + + // Replace route params like :id with actual values + Object.keys(route.params).forEach(key => { + path = path.replace(`:${key}`, route.params[key]); + }); + const nextUrl = path ? `${url}/${path}` : url; + const label = this._resolveLabel(route, path, nextUrl); + const skipLink = route.routeConfig.data?.['skipLink'] ?? false; + const icon = route.routeConfig.data?.['icon']; + + if (label && label.trim() !== '') { + breadcrumbs.push({label, url: nextUrl, skipLink, icon}); + } + + return route.firstChild + ? this.buildBreadcrumbs(route.firstChild, nextUrl, breadcrumbs) + : breadcrumbs; + } + + private _toTitleCase(str: string): string { + return str.replace(/-/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); + } + + private _resolveLabel( + route: ActivatedRouteSnapshot, + path: string, + currentUrl: string, + ): string { + const data = route.routeConfig?.data; + + //async breadcrumb logic + const asyncConfig = data?.asyncBreadcrumb; + if (asyncConfig?.service && asyncConfig?.method) { + const params = route.paramMap; + const paramValue = Object.fromEntries( + params.keys.map(k => [k, params.get(k)]), + ); + const fallback = + asyncConfig.fallbackLabel?.(params) || this._toTitleCase(path); + const loadingLabel = asyncConfig.loadingLabel || fallback; + + this.loading$.next(true); + try { + const serviceInstance = this.injector.get(asyncConfig.service); + const result$ = serviceInstance[asyncConfig.method](paramValue); + + this.loading$.next(true); + + result$ + .pipe( + tap(result => + this.updateBreadcrumbLabel(currentUrl, String(result)), + ), + catchError(error => { + console.warn('Async breadcrumb load failed:', error); + return EMPTY; + }), + finalize(() => this.loading$.next(false)), + ) + .subscribe(); + } catch (error) { + console.warn('Async breadcrumb load failed:', error); + this.loading$.next(false); + } + + return loadingLabel; + } + const breadcrumbData = data?.['breadcrumb']; + + if (typeof breadcrumbData === 'string') { + return breadcrumbData; + } + + return this._toTitleCase(path); + } + updateBreadcrumbLabel(url: string, newLabel: string): void { + const currentBreadcrumbs = this.breadcrumbs$.getValue(); + const index = currentBreadcrumbs.findIndex(bc => bc.url === url); + if (index !== -1) { + currentBreadcrumbs[index] = { + ...currentBreadcrumbs[index], + label: newLabel, + }; + this.breadcrumbs$.next([...currentBreadcrumbs]); + } + } + get breadcrumbs() { + return this.breadcrumbs$.asObservable(); + } + get loading(): Observable { + return this.loading$.asObservable(); + } +} diff --git a/projects/arc-lib/src/lib/components/index.ts b/projects/arc-lib/src/lib/components/index.ts index 6f21206f..5e46f739 100644 --- a/projects/arc-lib/src/lib/components/index.ts +++ b/projects/arc-lib/src/lib/components/index.ts @@ -4,3 +4,4 @@ export * from './gantt/gantt.module'; export * from './selector/select.module'; export * from './resize/resize.module'; export * from './list/list.component'; +export * from './breadcrumb/breadcrumb.component'; diff --git a/projects/arc/src/app/app-routing.module.ts b/projects/arc/src/app/app-routing.module.ts index 583bf642..b14ce947 100644 --- a/projects/arc/src/app/app-routing.module.ts +++ b/projects/arc/src/app/app-routing.module.ts @@ -15,6 +15,7 @@ const routes: Routes = [ { path: 'main', loadChildren: () => import('./main/main.module').then(m => m.MainModule), + data: {skipLink: true}, canActivate: [AuthGuard], }, { diff --git a/projects/arc/src/app/app.module.ts b/projects/arc/src/app/app.module.ts index 24ba47bb..1652bde6 100644 --- a/projects/arc/src/app/app.module.ts +++ b/projects/arc/src/app/app.module.ts @@ -19,10 +19,14 @@ import {environment} from '../environments/environment'; import {ThemeModule} from '@project-lib/theme/theme.module'; import {OverlayModule} from '@angular/cdk/overlay'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import {GanttModule} from '@project-lib/components/index'; +import {BreadcrumbComponent, GanttModule} from '@project-lib/components/index'; import {SelectModule} from '@project-lib/components/selector'; import {HeaderComponent} from '@project-lib/components/header/header.component'; import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.component'; +import {UserService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user/user.service'; +import {TitleService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service'; +import {NbIconModule} from '@nebular/theme'; +import {NbEvaIconsModule} from '@nebular/eva-icons'; @NgModule({ declarations: [AppComponent], @@ -40,6 +44,9 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen BrowserAnimationsModule, HeaderComponent, SidebarComponent, + BreadcrumbComponent, + NbIconModule, + NbEvaIconsModule, ], providers: [ TranslationService, @@ -48,6 +55,8 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen TranslateStore, SystemStoreFacadeService, EnvAdapterService, + UserService, + TitleService, ApiService, { provide: APP_CONFIG, diff --git a/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.html b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.html new file mode 100644 index 00000000..00f91ef9 --- /dev/null +++ b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.html @@ -0,0 +1,197 @@ + diff --git a/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.scss b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.scss new file mode 100644 index 00000000..bd1fce85 --- /dev/null +++ b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.scss @@ -0,0 +1,149 @@ +.breadcrumb-section { + padding: 1rem; + font-family: Arial, sans-serif; + background: #fff; + + h3 { + margin-top: 2rem; + margin-bottom: 0.5rem; + color: #333; + } + + .breadcrumb { + display: flex; + align-items: center; + flex-wrap: wrap; + list-style: none; + padding: 0; + margin-bottom: 1rem; + + li { + color: #555; + font-size: 14px; + + a { + color: #007bff; + text-decoration: none; + + &:hover { + text-decoration: underline; + } + } + + &.clickable { + cursor: pointer; + color: #007bff; + font-weight: bold; + } + + select { + padding: 2px 6px; + font-size: 14px; + } + } + + .separator { + margin: 0 8px; + color: #999; + } + } + + .expanded-breadcrumb { + margin-top: 0.5rem; + } +} +// example.component.scss + +.code-container { + display: flex; + gap: 2rem; + flex-wrap: wrap; + + // For smaller screens stack vertically + @media (max-width: 768px) { + flex-direction: column; + } +} + +.code-block { + position: relative; // needed for absolute positioning of icon + flex: 1 1 45%; + background-color: #f9f9f9; + padding: 1.5rem 1rem 1rem 1rem; // top padding extra for icon space + border-radius: 8px; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.1); + + pre { + background-color: #eee; + padding: 1rem; + border-radius: 5px; + overflow-x: auto; + font-family: monospace; + white-space: pre-wrap; + word-break: break-word; + } + + .copy-icon { + position: absolute; + top: 0.5rem; + right: 0.5rem; + cursor: pointer; + font-size: 1.25rem; + color: #3f51b5; + user-select: none; + transition: color 0.3s ease; + + &:hover { + color: #303f9f; + } + + &:active { + color: #283593; + } + } +} +.button-group { + display: flex; + gap: 2rem; // adjust spacing as needed + margin-bottom: 1rem; +} +.colors { + background-color: #1976d2; // blue color + color: white; + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s ease; + + &:hover { + background-color: #1565c0; // darker on hover + } + + &:active { + background-color: #0d47a1; // even darker when clicked + } +} +.breadcrumb-api-table { + width: 100%; + border-collapse: collapse; + margin-top: 1rem; + font-size: 16px; /* Increase font size */ + font-weight: bold; /* Make all text bold */ + + th, + td { + border: 1px solid #ccc; + padding: 10px 14px; + text-align: left; + } + + th { + background-color: #f0f0f0; + } + + tr:nth-child(even) { + background-color: #f9f9f9; + } +} diff --git a/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.ts b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.ts new file mode 100644 index 00000000..4462bc55 --- /dev/null +++ b/projects/arc/src/app/main/bread-crumb-introduction/bread-crumb-introduction.component.ts @@ -0,0 +1,120 @@ +import {CommonModule} from '@angular/common'; +import {Component, NgModule} from '@angular/core'; +import {Router} from '@angular/router'; +import {NbIconModule} from '@nebular/theme'; +@Component({ + selector: 'arc-bread-crumb-introduction', + templateUrl: './bread-crumb-introduction.component.html', + styleUrls: ['./bread-crumb-introduction.component.scss'], + standalone: true, + imports: [CommonModule, NbIconModule], +}) +export class BreadCrumbIntroductionComponent { + expanded = false; + constructor(private readonly router: Router) {} + basicCode = ` + + `; + routingCode = ` + { + path: 'user/:id', + component: UserComponent, + data: { + asyncBreadcrumb: { + service: UserService, + method: 'getUserNameForBreadcrumb', + fallbackLabel: (params: ParamMap) => \`User #\${params.get('id')}\`, + loadingLabel: 'Loading user...', + }, + icon: 'person-outline', + }, + children: [ + { + path: 'document/:id', + component: UserTitleComponent, + data: { + asyncBreadcrumb: { + service: TitleService, + method: 'getTitleNameForBreadcrumb', + fallbackLabel: (params: ParamMap) => + \`Document #\${params.get('id')}\`, + loadingLabel: 'Loading document...', + }, + icon: 'file-text-outline', + }, + }, + ], + }, + `; + asyncLogicCode = ` + const asyncConfig = data?.asyncBreadcrumb; + if (asyncConfig?.service && asyncConfig?.method) { + const params = route.paramMap; + const paramValue = params.get('id'); + const fallback = + asyncConfig.fallbackLabel?.(params) || this._toTitleCase(path); + const loadingLabel = asyncConfig.loadingLabel || fallback; + + setTimeout(async () => { + try { + const serviceInstance = this.injector.get(asyncConfig.service); + const result$ = serviceInstance[asyncConfig.method](paramValue); + const result = await result$.toPromise(); + this.updateBreadcrumbLabel(currentUrl, result); + } catch (error) { + console.warn('Async breadcrumb load failed:', error); + } + }, 0); + + return loadingLabel; + } + `; + serviceCode = ` + getUserById(id: string): Observable { + const user = this.users.find(u => u.id === id); + return of(user); + } + getUserNameForBreadcrumb(id: string): Observable { + return this.getUserById(id).pipe( + map(user => user?.name || \`User #\${id}\`), + catchError(() => of(\`User #\${id}\`)), + ); + }`; + iconBreadcrumbCode = ` + +`; + + copyCode(text: string) { + navigator.clipboard.writeText(text); + } + goToUser(userId: number) { + this.router.navigate(['/main/user', userId]); + } + goToDocument(userId: number, documentId: number) { + this.router.navigate(['/main/user', userId, 'document', documentId]); + } +} diff --git a/projects/arc/src/app/main/constants/components.constant.ts b/projects/arc/src/app/main/constants/components.constant.ts index ddf3eb1d..57fc8e0e 100644 --- a/projects/arc/src/app/main/constants/components.constant.ts +++ b/projects/arc/src/app/main/constants/components.constant.ts @@ -34,6 +34,10 @@ export const COMPONENTS_ITEMS = [ pathMatch: 'prefix', image: '../../../assets/images/components/Rectangle 37.svg', }, + { + title: 'Breadcrumb', + link: '/main/components/arc-comp/breadcrumb', + }, { title: 'List', link: '/main/components/arc-comp', diff --git a/projects/arc/src/app/main/introduction/introduction-routing.module.ts b/projects/arc/src/app/main/introduction/introduction-routing.module.ts index cc3dc0f4..ca0e3f54 100644 --- a/projects/arc/src/app/main/introduction/introduction-routing.module.ts +++ b/projects/arc/src/app/main/introduction/introduction-routing.module.ts @@ -1,12 +1,17 @@ import {NgModule} from '@angular/core'; import {RouterModule, Routes} from '@angular/router'; import {IntroductionComponent} from './introduction.component'; +import {BreadCrumbIntroductionComponent} from '../bread-crumb-introduction/bread-crumb-introduction.component'; const routes: Routes = [ { path: '', component: IntroductionComponent, }, + { + path: 'breadcrumb', + component: BreadCrumbIntroductionComponent, + }, ]; @NgModule({ imports: [RouterModule.forChild(routes)], diff --git a/projects/arc/src/app/main/main-routing.module.ts b/projects/arc/src/app/main/main-routing.module.ts index 92baaa87..ffeef2e9 100644 --- a/projects/arc/src/app/main/main-routing.module.ts +++ b/projects/arc/src/app/main/main-routing.module.ts @@ -1,7 +1,10 @@ import {NgModule} from '@angular/core'; -import {RouterModule, Routes} from '@angular/router'; +import {ParamMap, RouterModule, Routes} from '@angular/router'; import {MainComponent} from './main.component'; -import {IntroductionComponent} from './introduction/introduction.component'; +import {UserComponent} from '@project-lib/components/breadcrumb/breadcrumb-demo/user/user.component'; +import {UserTitleComponent} from '@project-lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.component'; +import {UserService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user/user.service'; +import {TitleService} from '@project-lib/components/breadcrumb/breadcrumb-demo/user-title/user-title.service'; const routes: Routes = [ { @@ -15,7 +18,6 @@ const routes: Routes = [ }, { path: 'components', - component: IntroductionComponent, children: [ { path: 'nebular-comp', @@ -33,10 +35,38 @@ const routes: Routes = [ }, ], }, + { + path: 'user/:id', + component: UserComponent, + data: { + asyncBreadcrumb: { + service: UserService, + method: 'getUserNameForBreadcrumb', + fallbackLabel: (params: ParamMap) => `User #${params.get('id')}`, + loadingLabel: 'Loading user...', + }, + icon: 'person-outline', + }, + children: [ + { + path: 'document/:id', + component: UserTitleComponent, + data: { + asyncBreadcrumb: { + service: TitleService, + method: 'getTitleNameForBreadcrumb', + fallbackLabel: (params: ParamMap) => + `Document #${params.get('id')}`, + loadingLabel: 'Loading document...', + }, + icon: 'file-text-outline', + }, + }, + ], + }, ], }, ]; - @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], diff --git a/projects/arc/src/app/main/main.component.html b/projects/arc/src/app/main/main.component.html index 89a6b3f1..9617819d 100644 --- a/projects/arc/src/app/main/main.component.html +++ b/projects/arc/src/app/main/main.component.html @@ -26,6 +26,14 @@ + diff --git a/projects/arc/src/app/main/main.module.ts b/projects/arc/src/app/main/main.module.ts index e87733c2..fadab0ef 100644 --- a/projects/arc/src/app/main/main.module.ts +++ b/projects/arc/src/app/main/main.module.ts @@ -5,6 +5,7 @@ import {MainComponent} from './main.component'; import {ThemeModule} from '@project-lib/theme/theme.module'; import {HeaderComponent} from '@project-lib/components/header/header.component'; import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.component'; +import {BreadcrumbComponent} from '@project-lib/components/index'; @NgModule({ declarations: [MainComponent], @@ -14,6 +15,7 @@ import {SidebarComponent} from '@project-lib/components/sidebar/sidebar.componen ThemeModule, HeaderComponent, SidebarComponent, + BreadcrumbComponent, ], }) export class MainModule {} diff --git a/projects/saas-ui/src/app/main/components/add-plan/add-plan.component.ts b/projects/saas-ui/src/app/main/components/add-plan/add-plan.component.ts index b9267394..83d382e0 100644 --- a/projects/saas-ui/src/app/main/components/add-plan/add-plan.component.ts +++ b/projects/saas-ui/src/app/main/components/add-plan/add-plan.component.ts @@ -309,7 +309,7 @@ export class AddPlanComponent implements OnInit { updateFeatureDetails, this.activateRoute.snapshot.params.id, ) - .subscribe(respFeature => {}); + .subscribe(); } else { // Handle form validation errors if necessary console.error('Form is invalid'); diff --git a/projects/saas-ui/src/app/on-boarding/components/add-tenant/add-tenant.component.ts b/projects/saas-ui/src/app/on-boarding/components/add-tenant/add-tenant.component.ts index c17723e5..581ffdc5 100644 --- a/projects/saas-ui/src/app/on-boarding/components/add-tenant/add-tenant.component.ts +++ b/projects/saas-ui/src/app/on-boarding/components/add-tenant/add-tenant.component.ts @@ -1,15 +1,21 @@ -import { Location } from '@angular/common'; -import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; -import { NbToastrService } from '@nebular/theme'; -import { AnyObject } from '@project-lib/core/api'; -import { environment } from 'projects/saas-ui/src/environment'; -import { Lead } from '../../../shared/models'; -import { BillingPlanService } from '../../../shared/services/billing-plan-service'; -import { OnBoardingService } from '../../../shared/services/on-boarding-service'; - -declare var Stripe: any; +import {Location} from '@angular/common'; +import { + AfterViewInit, + Component, + ElementRef, + OnInit, + ViewChild, +} from '@angular/core'; +import {FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {ActivatedRoute, Router} from '@angular/router'; +import {NbToastrService} from '@nebular/theme'; +import {AnyObject} from '@project-lib/core/api'; +import {environment} from 'projects/saas-ui/src/environment'; +import {Lead} from '../../../shared/models'; +import {BillingPlanService} from '../../../shared/services/billing-plan-service'; +import {OnBoardingService} from '../../../shared/services/on-boarding-service'; +import {Stripe} from '@stripe/stripe-js'; +declare let Stripe: (key: string) => Stripe; @Component({ selector: 'app-add-tenant', @@ -36,11 +42,18 @@ export class AddTenantComponent implements OnInit, AfterViewInit { private billingPlanService: BillingPlanService, ) { this.addTenantForm = this.fb.group({ - key: ['', [Validators.required, Validators.maxLength(10), Validators.pattern('^[a-zA-Z][a-zA-Z0-9]*$')]], + key: [ + '', + [ + Validators.required, + Validators.maxLength(10), + Validators.pattern('^[a-zA-Z][a-zA-Z0-9]*$'), + ], + ], domains: [''], - planId: [null, Validators.required], // Mark planId as required - paymentMethod: ['payment_source'], // Specify Stripe as payment method - paymentToken: ['', Validators.required] // New FormControl for Stripe token + planId: [null, Validators.required], // Mark planId as required + paymentMethod: ['payment_source'], // Specify Stripe as payment method + paymentToken: ['', Validators.required], // New FormControl for Stripe token }); } @@ -67,14 +80,14 @@ export class AddTenantComponent implements OnInit, AfterViewInit { fontSmoothing: 'antialiased', fontSize: '16px', '::placeholder': { - color: '#aab7c4' - } + color: '#aab7c4', + }, }, invalid: { color: '#fa755a', - iconColor: '#fa755a' - } - } + iconColor: '#fa755a', + }, + }, }); this.cardElement.mount(this.cardNumberElement.nativeElement); @@ -83,7 +96,7 @@ export class AddTenantComponent implements OnInit, AfterViewInit { this.cardElement.on('change', (event: any) => { if (event.error) { this.toastrService.danger(event.error.message, 'Error'); - this.addTenantForm.get('paymentToken')?.setValue(''); // Clear paymentToken on error + this.addTenantForm.get('paymentToken')?.setValue(''); // Clear paymentToken on error } else if (event.complete) { this.generateStripeToken(); } @@ -92,7 +105,7 @@ export class AddTenantComponent implements OnInit, AfterViewInit { // Generate token and set it to the form when payment details are complete async generateStripeToken() { - const { token, error } = await this.stripe.createToken(this.cardElement); + const {token, error} = await this.stripe.createToken(this.cardElement); if (error) { this.toastrService.danger(error.message, 'Error'); } else { @@ -115,7 +128,7 @@ export class AddTenantComponent implements OnInit, AfterViewInit { this.onboardingService.addTenant(domainData, this.leadId).subscribe( () => this.router.navigate(['/tenant/registration/complete']), - error => this.toastrService.danger('Registration failed', 'Error') + error => this.toastrService.danger('Registration failed', 'Error'), ); } } @@ -128,13 +141,15 @@ export class AddTenantComponent implements OnInit, AfterViewInit { }, error => { this.toastrService.danger('Failed to fetch lead data', 'Error'); - } + }, ); } updateDomainFromEmail() { if (this.leadData && this.leadData.email) { - const emailDomain = this.leadData.email?.substring(this.leadData.email.lastIndexOf('@') + 1); + const emailDomain = this.leadData.email?.substring( + this.leadData.email.lastIndexOf('@') + 1, + ); if (emailDomain) { this.addTenantForm.get('domains').setValue(emailDomain); }