Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"styleclass",
"supabase",
"timegrid",
"TSHIRT",
"Turborepo",
"Uids",
"viewports"
Expand Down
6 changes: 6 additions & 0 deletions apps/lfx-pcc/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

@if (statistics()) {
<div class="flex flex-col gap-4">
<!-- Quick Actions -->
<lfx-card>
<div class="flex flex-col gap-2">
<h4 class="text-sm font-semibold text-gray-900 mb-2">Quick Actions</h4>
<button class="flex items-center gap-2 w-full p-2 text-sm text-left text-gray-700 hover:bg-gray-50 rounded transition-colors">
<i class="fa-light fa-eye text-blue-500"></i>
View Public Profile
</button>
<button class="flex items-center gap-2 w-full p-2 text-sm text-left text-gray-700 hover:bg-gray-50 rounded transition-colors">
<i class="fa-light fa-calendar text-green-500"></i>
View Meeting History
</button>
<button class="flex items-center gap-2 w-full p-2 text-sm text-left text-gray-700 hover:bg-gray-50 rounded transition-colors">
<i class="fa-light fa-chart-bar text-purple-500"></i>
Activity Report
</button>
</div>
</lfx-card>

<!-- Profile Statistics Header -->
<div class="flex items-center gap-2">
<i class="fa-light fa-chart-line text-blue-500"></i>
<h3 class="text-lg font-semibold text-gray-900 mb-0">Profile Statistics</h3>
</div>

<!-- Engagement Statistics -->
<div class="grid grid-cols-2 gap-3">
<!-- Committees -->
<lfx-card styleClass="text-center">
<div class="flex flex-col items-center gap-2">
<div class="bg-purple-100 p-2 rounded-lg">
<i class="fa-light fa-users text-purple-600"></i>
</div>
<div>
<p class="text-lg font-bold text-gray-900">{{ statistics()!.committees }}</p>
<p class="text-xs text-gray-500">Committees</p>
</div>
</div>
</lfx-card>

<!-- Meetings -->
<lfx-card styleClass="text-center">
<div class="flex flex-col items-center gap-2">
<div class="bg-amber-100 p-2 rounded-lg">
<i class="fa-light fa-video text-amber-600"></i>
</div>
<div>
<p class="text-lg font-bold text-gray-900">{{ statistics()!.meetings }}</p>
<p class="text-xs text-gray-500">Meetings</p>
</div>
</div>
</lfx-card>

<!-- Contributions -->
<lfx-card styleClass="text-center">
<div class="flex flex-col items-center gap-2">
<div class="bg-indigo-100 p-2 rounded-lg">
<i class="fa-light fa-code-commit text-indigo-600"></i>
</div>
<div>
<p class="text-lg font-bold text-gray-900">{{ statistics()!.contributions }}</p>
<p class="text-xs text-gray-500">Contributions</p>
</div>
</div>
</lfx-card>

<!-- Active Projects -->
<lfx-card styleClass="text-center">
<div class="flex flex-col items-center gap-2">
<div class="bg-red-100 p-2 rounded-lg">
<i class="fa-light fa-folder-open text-red-600"></i>
</div>
<div>
<p class="text-lg font-bold text-gray-900">{{ statistics()!.activeProjects }}</p>
<p class="text-xs text-gray-500">Active Projects</p>
</div>
</div>
</lfx-card>
</div>
</div>
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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<CombinedProfile | null>(null);

// Computed statistics
public readonly statistics: Signal<UserStatistics | null> = 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`;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="bg-white border-b border-gray-100 py-6 shadow-sm">
<div class="container mx-auto px-8">
<!-- Breadcrumb Navigation -->
<div class="mb-4">
<lfx-breadcrumb [model]="breadcrumbItems()">
<ng-template #item let-item>
<a [routerLink]="item.routerLink" class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-gray-600 transition-colors">
@if (item.icon) {
<i [class]="item.icon" class="text-gray-500"></i>
}
{{ item.label }}
</a>
</ng-template>
</lfx-breadcrumb>
</div>

<div class="flex flex-col md:flex-row md:items-start justify-between mb-6">
<div class="flex items-start gap-6">
<!-- Profile Avatar -->
<lfx-avatar
[label]="userInitials()"
size="xlarge"
shape="circle"
[ariaLabel]="profileTitle() + ' avatar'"
styleClass="bg-blue-50 border border-gray-200 w-16 h-16"
data-testid="profile-avatar">
</lfx-avatar>

<div class="flex flex-col gap-1">
<!-- Profile Title -->
<h1 class="text-2xl font-display font-semibold text-gray-900 mb-0" data-testid="profile-title">
{{ profileTitle() }}
</h1>

<!-- Profile Subtitle (Title at Organization) -->
@if (profileSubtitle(); as subtitle) {
<p class="text-black-600 text-sm mb-2" data-testid="profile-subtitle">
{{ subtitle }}
</p>
}
</div>
</div>

<!-- Profile Links -->
<div class="flex items-start gap-6">
<!-- GitHub Profile -->
<a class="text-sm text-gray-600 flex items-center gap-2" data-testid="profile-github-profile">
<i class="fa-brands fa-github text-[#181717] text-2xl cursor-pointer"></i>
</a>

<!-- LinkedIn Profile -->
<a class="text-sm text-gray-600 flex items-center gap-2" data-testid="profile-linkedin-profile">
<i class="fa-brands fa-linkedin text-[#0077b5] text-2xl cursor-pointer"></i>
</a>
</div>
</div>

<!-- Menu Items Section -->
<div class="md:flex items-center justify-start md:justify-between">
<div class="flex items-center gap-3 flex-wrap">
@for (menu of menuItems(); track menu.label) {
<a
[routerLink]="menu.routerLink"
class="pill"
routerLinkActive="bg-blue-50 text-blue-600 border-0"
data-testid="profile-menu-item"
[attr.data-menu-item]="menu.label"
[routerLinkActiveOptions]="menu.routerLinkActiveOptions">
@if (menu.icon) {
<i [class]="menu.icon"></i>
}
{{ menu.label }}
</a>
}
</div>

<!-- Member Since and Last Active -->
@if (profile()?.user && memberSince() && lastActive()) {
<div class="flex items-center gap-6 mt-4 md:mt-0" data-testid="profile-layout-membership-info">
<!-- Member Since -->
<div class="flex items-center gap-2 text-sm text-gray-600" data-testid="profile-layout-member-since">
<i class="fa-light fa-calendar-check text-blue-500"></i>
<span class="font-medium">Member Since:</span>
<span>{{ memberSince() }}</span>
</div>

<!-- Last Active -->
<div class="flex items-center gap-2 text-sm text-gray-600" data-testid="profile-layout-last-active">
<i class="fa-light fa-clock text-green-500"></i>
<span class="font-medium">Last Active:</span>
<span>{{ lastActive() }}</span>
</div>
</div>
}
</div>
</div>
</div>

<!-- Content Area -->
@if (!loading()) {
<div class="container mx-auto px-8 py-8">
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
<!-- Main Content (Left Side) -->
<div class="lg:col-span-3">
<router-outlet></router-outlet>
</div>

<!-- Statistics Sidebar (Right Side) -->
<div class="lg:col-span-1">
<lfx-profile-stats [profile]="profile()"></lfx-profile-stats>
</div>
</div>
</div>
} @else {
<div class="container mx-auto px-8 py-8">
<div class="flex flex-col gap-2 items-center justify-center py-12" data-testid="profile-loading">
<i class="fa-light fa-circle-notch fa-spin text-4xl text-blue-400" data-testid="loading-spinner"></i>
<span class="ml-3 text-gray-600">Loading profile...</span>
</div>
</div>
}
Original file line number Diff line number Diff line change
@@ -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%);
}
}
Loading