-
Notifications
You must be signed in to change notification settings - Fork 0
feat(profile): implement complete profile management system #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
04e03ab
feat(profile): implement complete profile management system
asithade 4acdc61
refactor(profile): move member info to layout and align data-testid
asithade e131fb6
fix(server): m2m auth0 issuer base url
asithade c41d8d7
fix(ui): reuse stats
asithade File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ | |
| "styleclass", | ||
| "supabase", | ||
| "timegrid", | ||
| "TSHIRT", | ||
| "Turborepo", | ||
| "Uids", | ||
| "viewports" | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
...-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); as stats) { | ||
| <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">{{ stats!.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">{{ stats!.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">{{ stats!.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">{{ stats!.activeProjects }}</p> | ||
| <p class="text-xs text-gray-500">Active Projects</p> | ||
| </div> | ||
| </div> | ||
| </lfx-card> | ||
| </div> | ||
| </div> | ||
| } |
5 changes: 5 additions & 0 deletions
5
...-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.scss
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
86 changes: 86 additions & 0 deletions
86
...fx-pcc/src/app/layouts/profile-layout/components/profile-stats/profile-stats.component.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| }; | ||
| }); | ||
asithade marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * 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`; | ||
| } | ||
| } | ||
asithade marked this conversation as resolved.
Show resolved
Hide resolved
|
||
124 changes: 124 additions & 0 deletions
124
apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| } | ||
asithade marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| {{ 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> | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
apps/lfx-pcc/src/app/layouts/profile-layout/profile-layout.component.scss
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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%); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.