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 @@
+
+
+
+
{{ separator }}
+
+
{{ separator }}
+
+
+
+
+
+
+
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 @@
+
+
1.Basic Breadcrumb
+
+ Home
+ ›
+ Users
+ ›
+ John Doe
+ ›
+ Document.pdf
+
+
+
+
2. Dynamic Handling Breadcrumb
+
+ This example demonstrates how breadcrumbs can be dynamically generated and
+ updated based on route parameters and asynchronous data fetching from
+ services. Instead of relying on blocking resolvers, the breadcrumb labels
+ are initially displayed using fallback or loading placeholders (e.g.,
+ "Loading user...") and are automatically updated when the actual data
+ becomes available from the API or service. This approach ensures smooth
+ navigation and improves user experience by preventing full page loading
+ delays while keeping the breadcrumb context accurate and dynamic.
+
+
+
+ Main
+ ›
+ ...
+ ›
+ Document.pdf
+
+
+
+
+ Main
+ ›
+ Users
+ ›
+ John Doe
+ ›
+ Document.pdf
+
+
+
+ User/123
+
+ User/124/document/1
+
+
+
+
+
Routing Code
+
📋
+
{{ routingCode }}
+
+
+
+
Service Code
+
📋
+
{{ serviceCode }}
+
+
+
+
Async Breadcrumb Logic
+
📋
+
{{ asyncLogicCode }}
+
+
+
3. Breadcrumb with Icons
+
+ This example shows how to add icons to breadcrumb items using icon property
+ in route data.
+
+
+
+
+
+ Home
+
+ ›
+
+
+ Users
+
+ ›
+
+
+ Jane Smith
+
+ ›
+
+
+ Contract.pdf
+
+
+
+
+
📋
+
{{ iconBreadcrumbCode }}
+
+
+
4. Breadcrumb Inputs
+
See all available inputs for customization:
+
+
+
+
+ Input
+ Description
+
+
+
+
+ staticBreadcrumbs
+ Static breadcrumb items
+
+
+ showIcons
+
+ Icons will appear before each breadcrumb item that has an associated
+ icon
+
+
+
+ separator
+ Separator character/string
+
+
+ skipLink
+ A flag to disable click navigation on a breadcrumb
+
+
+ separatorClass
+ Custom class applied to the separator between breadcrumb items
+
+
+ itemClass
+ Custom class applied to each breadcrumb element.
+
+
+ maxLabelLength
+
+ Maximum number of characters shown in each breadcrumb label before it
+ gets trimmed with an ellipsis (…)
+
+
+
+ maxItems
+
+ Maximum number of breadcrumb items to display before collapsing the
+ middle items into an expandable “...” element
+
+
+
+
+
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 = `
+
+
+
+ Home
+
+ ›
+
+
+ Users
+
+ ›
+
+
+ Jane Smith
+
+ ›
+
+
+ Contract.pdf
+
+
+`;
+
+ 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);
}