diff --git a/.vscode/settings.json b/.vscode/settings.json
index f0381fe8..c31d0e61 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -26,6 +26,7 @@
"styleclass",
"supabase",
"timegrid",
+ "TSHIRT",
"Turborepo",
"Uids",
"viewports"
diff --git a/apps/lfx-pcc/.env.example b/apps/lfx-pcc/.env.example
index fb5b92a5..184967a3 100644
--- a/apps/lfx-pcc/.env.example
+++ b/apps/lfx-pcc/.env.example
@@ -14,7 +14,7 @@ PCC_AUTH0_SECRET=sufficiently-long-string
# M2M Token Generation
M2M_AUTH_CLIENT_ID=your-auth0-client-id
M2M_AUTH_CLIENT_SECRET=your-auth0-client-secret
-M2M_AUTH_ISSUER_BASE_URL=https://auth.k8s.orb.local
+M2M_AUTH_ISSUER_BASE_URL=https://auth.k8s.orb.local/
M2M_AUTH_AUDIENCE=http://lfx-api.k8s.orb.local/
# Microservice Configuration
diff --git a/apps/lfx-pcc/src/app/app.routes.ts b/apps/lfx-pcc/src/app/app.routes.ts
index d30b05a8..d5c9e453 100644
--- a/apps/lfx-pcc/src/app/app.routes.ts
+++ b/apps/lfx-pcc/src/app/app.routes.ts
@@ -22,4 +22,10 @@ export const routes: Routes = [
canActivate: [authGuard],
data: { preload: true, preloadDelay: 1000 }, // Preload after 1 second for likely navigation
},
+ {
+ path: 'profile',
+ loadComponent: () => import('./layouts/profile-layout/profile-layout.component').then((m) => m.ProfileLayoutComponent),
+ loadChildren: () => import('./modules/profile/profile.routes').then((m) => m.PROFILE_ROUTES),
+ canActivate: [authGuard],
+ },
];
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.html b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.html
new file mode 100644
index 00000000..38e54436
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.html
@@ -0,0 +1,86 @@
+
+
+
+@if (statistics(); as stats) {
+
+
+
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
Profile Statistics
+
+
+
+
+
+
+
+
+
+
+
+
{{ stats!.committees }}
+
Committees
+
+
+
+
+
+
+
+
+
+
+
+
{{ stats!.meetings }}
+
Meetings
+
+
+
+
+
+
+
+
+
+
+
+
{{ stats!.contributions }}
+
Contributions
+
+
+
+
+
+
+
+
+
+
+
+
{{ stats!.activeProjects }}
+
Active Projects
+
+
+
+
+
+}
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.scss b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.scss
new file mode 100644
index 00000000..60a8ab03
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.scss
@@ -0,0 +1,5 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+// Additional styles for profile stats component
+// Currently using Tailwind classes, custom styles can be added here if needed
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.ts b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.ts
new file mode 100644
index 00000000..070dc2ee
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.ts
@@ -0,0 +1,86 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { CommonModule } from '@angular/common';
+import { Component, computed, input, Signal } from '@angular/core';
+import { CombinedProfile, UserStatistics } from '@lfx-pcc/shared/interfaces';
+import { CardComponent } from '@shared/components/card/card.component';
+
+@Component({
+ selector: 'lfx-profile-stats',
+ standalone: true,
+ imports: [CommonModule, CardComponent],
+ templateUrl: './profile-stats.component.html',
+ styleUrl: './profile-stats.component.scss',
+})
+export class ProfileStatsComponent {
+ // Input profile data
+ public readonly profile = input(null);
+
+ // Computed statistics
+ public readonly statistics: Signal = computed(() => {
+ const profileData = this.profile();
+ if (!profileData?.user) return null;
+
+ return {
+ committees: 5, // Mock data
+ meetings: 23, // Mock data
+ contributions: 147, // Mock data
+ activeProjects: 3, // Mock data
+ memberSince: this.calculateMemberSince(profileData.user.created_at),
+ lastActive: this.calculateLastActive(profileData.user.updated_at),
+ };
+ });
+
+ /**
+ * Calculate how long the user has been a member
+ */
+ private calculateMemberSince(createdAt: string): string {
+ const created = new Date(createdAt);
+ const now = new Date();
+ const diffTime = Math.abs(now.getTime() - created.getTime());
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays < 30) {
+ return `${diffDays} day${diffDays === 1 ? '' : 's'}`;
+ } else if (diffDays < 365) {
+ const months = Math.floor(diffDays / 30);
+ return `${months} month${months === 1 ? '' : 's'}`;
+ }
+
+ const years = Math.floor(diffDays / 365);
+ const remainingMonths = Math.floor((diffDays % 365) / 30);
+ if (remainingMonths > 0) {
+ return `${years} year${years === 1 ? '' : 's'}, ${remainingMonths} month${remainingMonths === 1 ? '' : 's'}`;
+ }
+ return `${years} year${years === 1 ? '' : 's'}`;
+ }
+
+ /**
+ * Calculate last active time
+ */
+ private calculateLastActive(updatedAt: string): string {
+ const updated = new Date(updatedAt);
+ const now = new Date();
+ const diffTime = Math.abs(now.getTime() - updated.getTime());
+ const diffMinutes = Math.floor(diffTime / (1000 * 60));
+ const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffMinutes < 1) {
+ return 'Just now';
+ } else if (diffMinutes < 60) {
+ return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
+ } else if (diffHours < 24) {
+ return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
+ } else if (diffDays < 30) {
+ return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
+ } else if (diffDays < 365) {
+ const months = Math.floor(diffDays / 30);
+ return `${months} month${months === 1 ? '' : 's'} ago`;
+ }
+
+ const years = Math.floor(diffDays / 365);
+ return `${years} year${years === 1 ? '' : 's'} ago`;
+ }
+}
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.html b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.html
new file mode 100644
index 00000000..88849f18
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.html
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ profileTitle() }}
+
+
+
+ @if (profileSubtitle(); as subtitle) {
+
+ {{ subtitle }}
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+ @if (profile()?.user && memberSince() && lastActive()) {
+
+
+
+
+ Member Since:
+ {{ memberSince() }}
+
+
+
+
+
+ Last Active:
+ {{ lastActive() }}
+
+
+ }
+
+
+
+
+
+@if (!loading()) {
+
+} @else {
+
+
+
+ Loading profile...
+
+
+}
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.scss b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.scss
new file mode 100644
index 00000000..556bc71c
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.scss
@@ -0,0 +1,18 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+// Profile layout specific styles
+// Most styling comes from Tailwind classes in the template
+// This file is reserved for any custom styling that can't be achieved with Tailwind
+
+:host {
+ display: block;
+
+ .pill {
+ @apply inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-full transition-colors text-gray-600 border border-gray-200 hover:bg-gray-50;
+ }
+
+ .profile-avatar-placeholder {
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
+ }
+}
diff --git a/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.ts b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.ts
new file mode 100644
index 00000000..e401d587
--- /dev/null
+++ b/apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.ts
@@ -0,0 +1,229 @@
+// Copyright The Linux Foundation and each contributor to LFX.
+// SPDX-License-Identifier: MIT
+
+import { CommonModule } from '@angular/common';
+import { Component, computed, inject, input, Signal, signal } from '@angular/core';
+import { toSignal } from '@angular/core/rxjs-interop';
+import { RouterModule } from '@angular/router';
+import { CombinedProfile } from '@lfx-pcc/shared/interfaces';
+import { UserService } from '@services/user.service';
+import { AvatarComponent } from '@shared/components/avatar/avatar.component';
+import { BreadcrumbComponent } from '@shared/components/breadcrumb/breadcrumb.component';
+import { MenuItem } from 'primeng/api';
+import { ChipModule } from 'primeng/chip';
+import { finalize } from 'rxjs';
+
+import { ProfileStatsComponent } from './components/profile-stats/profile-stats.component';
+
+@Component({
+ selector: 'lfx-profile-layout',
+ standalone: true,
+ imports: [CommonModule, RouterModule, AvatarComponent, BreadcrumbComponent, ChipModule, ProfileStatsComponent],
+ templateUrl: './profile-layout.component.html',
+ styleUrl: './profile-layout.component.scss',
+})
+export class ProfileLayoutComponent {
+ private readonly userService = inject(UserService);
+
+ // Load current user profile data
+ public loading = signal(true);
+
+ public profile = this.initializeProfile();
+ // Computed profile data
+ public readonly profileTitle = this.initializeProfileTitle();
+ public readonly profileSubtitle = this.initializeProfileSubtitle();
+ public readonly profileLocation = this.initializeProfileLocation();
+ public readonly userInitials = this.initializeUserInitials();
+ public readonly memberSince = this.initializeMemberSince();
+ public readonly lastActive = this.initializeLastActive();
+ // Loading state
+
+ public readonly breadcrumbItems = input