Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,13 @@ <h2 class="text-lg font-semibold text-gray-900">Menu</h2>
}

<!-- Main Content Area -->
<main class="flex-1 min-w-0 transition-all duration-300 lg:ml-64 px-[21px] md:px-[32px] py-[24px]" data-testid="main-content">
<router-outlet />
<!-- Footer Component -->
<lfx-footer class="py-8"></lfx-footer>
<main class="flex-1 min-w-0 transition-all duration-300 lg:ml-64" data-testid="main-content">
<div class="w-full max-w-[1440px] mx-auto px-8 py-8">
<div class="max-w-[1245px]">
<router-outlet />
</div>
<!-- Footer Component -->
<lfx-footer class="py-8"></lfx-footer>
</div>
</main>
</div>
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<!-- Copyright The Linux Foundation and each contributor to LFX. -->
<!-- SPDX-License-Identifier: MIT -->

<div class="container mx-auto px-4 sm:px-6 lg:px-8" data-testid="dashboard-container">
<div data-testid="dashboard-container">
<!-- Foundation Project -->
@if (selectedFoundation()) {
<div class="mb-6 flex items-center gap-4" data-testid="foundation-project">
<h1 class="text-2xl font-serif font-semibold text-gray-900">{{ selectedFoundation()?.name }} Overview</h1>
<h1>{{ selectedFoundation()?.name }} Overview</h1>
</div>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<section data-testid="foundation-health-section">
<div class="flex items-center justify-between mb-4">
<div class="flex flex-col md:flex-row md:items-center gap-3">
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ title() }}</h2>
<h2>{{ title() }}</h2>

<!-- Filter Pills -->
<lfx-filter-pills
Expand Down Expand Up @@ -36,15 +36,25 @@ <h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ title() }}</h2>
</div>

<!-- Carousel Container -->
<div class="overflow-hidden">
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel">
<div class="relative overflow-hidden">
<!-- Left Fade Gradient -->
@if (canScrollLeft()) {
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);"></div>
}

<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
Comment on lines +42 to +47
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +47
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
}

<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="foundation-health-carousel" (scroll)="onScroll()">
@for (card of metricCards(); track card.title) {
<div class="p-4 bg-white rounded-lg border border-slate-200 flex-shrink-0 w-[calc(100vw-3rem)] md:w-80" [attr.data-testid]="card.testId">
<div class="p-4 bg-white rounded-lg flex-shrink-0 w-[calc(100vw-3rem)] md:w-80 shadow-[0px_1px_2px_-1px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10)]" [attr.data-testid]="card.testId">
<div class="flex flex-col h-full justify-between">
<!-- Card Header -->
<div class="flex items-center gap-2">
<i [class]="card.icon + ' w-4 h-4 text-muted-foreground'"></i>
<h5 class="text-sm font-medium">{{ card.title }}</h5>
<h4>{{ card.title }}</h4>
</div>

<!-- Custom Content -->
Expand Down Expand Up @@ -72,12 +82,14 @@ <h5 class="text-sm font-medium">{{ card.title }}</h5>
<!-- Top Projects List -->
@case ('top-projects') {
@if (card.topProjects) {
<div class="space-y-0.5 p-0 pr-5 pl-[60px] m-0 mb-[5px]">
<div class="text-xs font-medium text-muted-foreground">Top 3 Projects by Value</div>
<div class="space-y-0.5 p-[0px] mt-[0px] mr-[0px] mb-[5px] ml-[0px] flex flex-col items-end">
<div class="flex items-center justify-between w-[180px]">
<span class="text-xs text-muted-foreground">Top 3 Projects by Value</span>
</div>
@for (project of card.topProjects; track project.name) {
<div class="flex items-center justify-between text-sm">
<span class="text-muted-foreground">{{ project.name }}</span>
<span class="font-medium">{{ project.formattedValue }}</span>
<div class="flex items-center justify-between w-[180px]">
<span class="text-xs text-muted-foreground">{{ project.name }}</span>
<span class="font-medium text-xs">{{ project.formattedValue }}</span>
</div>
}
</div>
Expand Down Expand Up @@ -137,7 +149,7 @@ <h5 class="text-sm font-medium">{{ card.title }}</h5>
<div class="text-xl font-medium">{{ card.value }}</div>
}
@if (card.subtitle) {
<div class="text-xs text-muted-foreground">{{ card.subtitle }}</div>
<div class="text-xs text-gray-500">{{ card.subtitle }}</div>
}
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, ElementRef, signal, ViewChild, input } from '@angular/core';
import { AfterViewInit, Component, computed, ElementRef, signal, ViewChild, input } from '@angular/core';
import { ChartComponent } from '@components/chart/chart.component';
import { FilterOption, FilterPillsComponent } from '@components/filter-pills/filter-pills.component';
import { AGGREGATE_FOUNDATION_METRICS, FOUNDATION_SPARKLINE_CHART_OPTIONS, FOUNDATION_BAR_CHART_OPTIONS } from '@lfx-one/shared/constants';
Expand All @@ -16,11 +16,14 @@ import { hexToRgba } from '@lfx-one/shared/utils';
templateUrl: './foundation-health.component.html',
styleUrl: './foundation-health.component.scss',
})
export class FoundationHealthComponent {
export class FoundationHealthComponent implements AfterViewInit {
@ViewChild('carouselScroll') public carouselScrollContainer!: ElementRef;

public readonly title = input<string>('Foundation Health');

public readonly canScrollLeft = signal<boolean>(false);
public readonly canScrollRight = signal<boolean>(false);

public readonly selectedFilter = signal<string>('all');

public readonly filterOptions: FilterOption[] = [
Expand All @@ -34,6 +37,11 @@ export class FoundationHealthComponent {

public readonly barChartOptions = FOUNDATION_BAR_CHART_OPTIONS;

public ngAfterViewInit(): void {
// Initialize scroll state after view is ready
setTimeout(() => this.onScroll(), 0);
}

private readonly allMetricCards = computed<FoundationMetricCard[]>(() => {
const metrics = AGGREGATE_FOUNDATION_METRICS;

Expand Down Expand Up @@ -62,7 +70,7 @@ export class FoundationHealthComponent {
icon: 'fa-light fa-chart-bar',
title: 'Software Value',
value: this.formatSoftwareValue(metrics.softwareValue),
subtitle: 'Estimated total value of software managed',
subtitle: "Estimated total value of all foundation's projects",
category: 'projects' as MetricCategory,
testId: 'foundation-health-card-software-value',
customContentType: 'top-projects',
Expand Down Expand Up @@ -168,6 +176,18 @@ export class FoundationHealthComponent {
container.scrollBy({ left: 320, behavior: 'smooth' });
}

public onScroll(): void {
if (!this.carouselScrollContainer?.nativeElement) return;
const container = this.carouselScrollContainer.nativeElement;

// Check if can scroll left (not at the start)
this.canScrollLeft.set(container.scrollLeft > 0);

// Check if can scroll right (not at the end)
const maxScrollLeft = container.scrollWidth - container.clientWidth;
this.canScrollRight.set(container.scrollLeft < maxScrollLeft - 1);
}

private formatSoftwareValue(valueInMillions: number): string {
if (valueInMillions >= 1000) {
const billions = valueInMillions / 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<section class="flex flex-col flex-1" data-testid="dashboard-my-meetings-section">
<!-- Header -->
<div class="flex items-center justify-between mb-4">
<h2 class="font-display font-semibold text-gray-900">My Meetings</h2>
<h2>My Meetings</h2>
<lfx-button
label="View All"
icon="fa-light fa-chevron-right"
Expand All @@ -17,19 +17,29 @@ <h2 class="font-display font-semibold text-gray-900">My Meetings</h2>
</div>

<!-- Scrollable Content -->
<div class="flex flex-col flex-1">
<div class="relative flex flex-col flex-1 -my-1">
@if (loading()) {
<div class="flex flex-col gap-3" data-testid="dashboard-my-meetings-loading">
<p-skeleton width="100%" height="140px"></p-skeleton>
<p-skeleton width="100%" height="140px"></p-skeleton>
</div>
} @else {
<div class="flex flex-col gap-6 overflow-scroll max-h-[30rem]" data-testid="dashboard-my-meetings-list">
<!-- Top Fade Gradient -->
@if (canScrollUp()) {
<div class="absolute top-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to bottom, #f8f8f9 0%, transparent 100%);"></div>
}

<!-- Bottom Fade Gradient -->
@if (canScrollDown()) {
<div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to top, #f8f8f9 0%, transparent 100%);"></div>
Comment on lines +29 to +34
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="absolute top-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to bottom, #f8f8f9 0%, transparent 100%);"></div>
}
<!-- Bottom Fade Gradient -->
@if (canScrollDown()) {
<div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to top, #f8f8f9 0%, transparent 100%);"></div>
<div class="absolute top-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to bottom, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>
}
<!-- Bottom Fade Gradient -->
@if (canScrollDown()) {
<div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to top, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to top, #f8f8f9 0%, transparent 100%);"></div>
<div class="absolute bottom-0 left-0 right-0 h-16 pointer-events-none z-10" style="background: linear-gradient(to top, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
}

<div #scrollContainer class="flex flex-col gap-6 overflow-scroll max-h-[30rem] px-1 -mx-1 py-1" (scroll)="onScroll()" data-testid="dashboard-my-meetings-list">
@if (todayMeetings().length > 0 || upcomingMeetings().length > 0) {
<!-- TODAY Section - only show if there are meetings today -->
@if (todayMeetings().length > 0) {
<div>
<h4 class="text-xs font-medium text-gray-500 mb-3 uppercase tracking-wide">Today</h4>
<h4 class="text-gray-500 mb-3">Today</h4>
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The h4 element has text-gray-500 class which will override the default typography styles defined in styles.scss. The new global styles target elements without text- or font- classes using :not([class*='text-']):not([class*='font-']). Consider removing the explicit text color class to let the default h4 styles apply, or keep it intentionally for specific styling. Additionally, the removed classes (text-xs font-medium uppercase tracking-wide) provided specific styling that is now lost - verify this is the intended visual outcome.

Copilot uses AI. Check for mistakes.
<div class="flex flex-col gap-3">
@for (item of todayMeetings(); track item.meeting.uid) {
<lfx-dashboard-meeting-card
Expand All @@ -44,7 +54,7 @@ <h4 class="text-xs font-medium text-gray-500 mb-3 uppercase tracking-wide">Today
<!-- UPCOMING Section - only show if there are upcoming meetings -->
@if (upcomingMeetings().length > 0) {
<div>
<h4 class="text-xs font-medium text-gray-500 mb-3 uppercase tracking-wide">Upcoming</h4>
<h4 class="text-gray-500 mb-3">Upcoming</h4>
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The h4 element has text-gray-500 class which will override the default typography styles defined in styles.scss. The new global styles target elements without text- or font- classes using :not([class*='text-']):not([class*='font-']). Consider removing the explicit text color class to let the default h4 styles apply, or keep it intentionally for specific styling. Additionally, the removed classes (text-xs font-medium uppercase tracking-wide) provided specific styling that is now lost - verify this is the intended visual outcome.

Copilot uses AI. Check for mistakes.
<div class="flex flex-col gap-3">
@for (item of upcomingMeetings(); track item.meeting.uid) {
<lfx-dashboard-meeting-card
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, inject, signal } from '@angular/core';
import { AfterViewInit, Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Router } from '@angular/router';
import { MeetingService } from '@app/shared/services/meeting.service';
Expand All @@ -21,12 +21,34 @@ import type { MeetingWithOccurrence } from '@lfx-one/shared/interfaces';
templateUrl: './my-meetings.component.html',
styleUrl: './my-meetings.component.scss',
})
export class MyMeetingsComponent {
export class MyMeetingsComponent implements AfterViewInit {
@ViewChild('scrollContainer') private scrollContainer!: ElementRef;

private readonly meetingService = inject(MeetingService);
private readonly router = inject(Router);
protected readonly loading = signal(true);
private readonly allMeetings = toSignal(this.meetingService.getMeetings().pipe(finalize(() => this.loading.set(false))), { initialValue: [] });

public readonly canScrollUp = signal<boolean>(false);
public readonly canScrollDown = signal<boolean>(false);

public ngAfterViewInit(): void {
// Initialize scroll state after view is ready
setTimeout(() => this.onScroll(), 0);
}

public onScroll(): void {
if (!this.scrollContainer?.nativeElement) return;
const container = this.scrollContainer.nativeElement;

// Check if can scroll up (not at the top)
this.canScrollUp.set(container.scrollTop > 0);

// Check if can scroll down (not at the bottom)
const maxScrollTop = container.scrollHeight - container.clientHeight;
this.canScrollDown.set(container.scrollTop < maxScrollTop - 1);
}

protected readonly todayMeetings = computed<MeetingWithOccurrence[]>(() => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

<div data-testid="dashboard-my-projects-section">
<div class="flex items-center justify-between mb-4">
<h2 class="font-display font-semibold text-[16px]">My Projects</h2>
<h2>My Projects</h2>
</div>

<div class="rounded-lg border border-gray-200 overflow-hidden relative">
<div class="rounded-lg overflow-hidden relative shadow-[0px_1px_2px_-1px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10)]">
@if (loading()) {
<div class="absolute inset-0 bg-white/60 flex items-center justify-center z-10" data-testid="dashboard-my-projects-loading">
<div class="flex flex-col items-center gap-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<section data-testid="dashboard-organization-involvement-section">
<div class="flex items-center justify-between mb-4">
<div class="flex flex-col md:flex-row md:items-center gap-3">
<h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ accountName() }}'s Involvement</h2>
<h2>{{ accountName() }}'s Involvement</h2>

<!-- Filter Pills -->
<lfx-filter-pills [options]="filterOptions" [selectedFilter]="selectedFilter()" (filterChange)="handleFilterChange($event)"></lfx-filter-pills>
Expand Down Expand Up @@ -49,18 +49,28 @@ <h2 class="font-['Roboto_Slab'] font-semibold text-[16px]">{{ accountName() }}'s
</div>
</div>
} @else {
<div class="overflow-hidden">
<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="dashboard-involvement-carousel">
<div class="relative overflow-hidden">
<!-- Left Fade Gradient -->
@if (canScrollLeft()) {
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);"></div>
}

<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +60
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gradient overlay div lacks an aria-hidden="true" attribute. Since this is a purely decorative element that serves no semantic purpose, it should be hidden from assistive technologies to avoid confusion.

Suggested change
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);"></div>
<div class="hidden md:block absolute top-0 bottom-0 left-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to right, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>
}
<!-- Right Fade Gradient -->
@if (canScrollRight()) {
<div class="hidden md:block absolute top-0 bottom-0 right-0 w-24 pointer-events-none z-10" style="background: linear-gradient(to left, #f8f8f9 0%, transparent 100%);" aria-hidden="true"></div>

Copilot uses AI. Check for mistakes.
}

<div #carouselScroll class="flex gap-4 overflow-x-auto pb-2 hide-scrollbar scroll-smooth" data-testid="dashboard-involvement-carousel" (scroll)="onScroll()">
@for (metric of primaryMetrics(); track metric.title) {
<!-- Membership Tier Card (Special) -->
@if (metric.isMembershipTier) {
<div
class="p-4 bg-white rounded-lg border border-slate-200 hover:border-[#0094FF] transition-colors cursor-pointer flex-shrink-0 w-80"
class="p-4 bg-white rounded-lg hover:border hover:border-[#0094FF] transition-colors cursor-pointer flex-shrink-0 w-80 shadow-[0px_1px_2px_-1px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10)]"
[attr.data-testid]="'dashboard-involvement-metric-' + metric.title">
<div class="space-y-3">
<div class="flex items-center gap-1">
<i [class]="metric.icon + ' text-gray-500'" class="w-4 h-4"></i>
<h5 class="text-sm font-medium">{{ metric.title }}</h5>
<h4>{{ metric.title }}</h4>
</div>

<!-- Membership Info -->
Expand All @@ -84,11 +94,11 @@ <h5 class="text-sm font-medium">{{ metric.title }}</h5>
</div>
} @else {
<!-- Regular Card -->
<div class="p-4 bg-white rounded-lg border border-slate-200 flex-shrink-0 w-80" [attr.data-testid]="'dashboard-involvement-metric-' + metric.title">
<div class="p-4 bg-white rounded-lg flex-shrink-0 w-80 shadow-[0px_1px_2px_-1px_rgba(0,0,0,0.10),0px_1px_3px_0px_rgba(0,0,0,0.10)]" [attr.data-testid]="'dashboard-involvement-metric-' + metric.title">
<div class="space-y-3">
<div class="flex items-center gap-3">
<i [class]="metric.icon + ' text-gray-500'" class="w-4 h-4"></i>
<h5 class="text-sm font-medium">{{ metric.title }}</h5>
<h4>{{ metric.title }}</h4>
</div>
@if (metric.chartData) {
<div class="w-full h-16">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT

import { CommonModule } from '@angular/common';
import { Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
import { AfterViewInit, Component, computed, ElementRef, inject, signal, ViewChild } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { AccountContextService } from '@app/shared/services/account-context.service';
import { AnalyticsService } from '@app/shared/services/analytics.service';
Expand All @@ -22,13 +22,16 @@ import { combineLatest, finalize, map, of, switchMap } from 'rxjs';
templateUrl: './organization-involvement.component.html',
styleUrl: './organization-involvement.component.scss',
})
export class OrganizationInvolvementComponent {
export class OrganizationInvolvementComponent implements AfterViewInit {
@ViewChild('carouselScroll') public carouselScrollContainer!: ElementRef;

private readonly analyticsService = inject(AnalyticsService);
private readonly accountContextService = inject(AccountContextService);
private readonly projectContextService = inject(ProjectContextService);

public readonly canScrollLeft = signal<boolean>(false);
public readonly canScrollRight = signal<boolean>(false);

private readonly contributionsLoading = signal(true);
private readonly dashboardLoading = signal(true);
private readonly eventsLoading = signal(true);
Expand All @@ -50,6 +53,11 @@ export class OrganizationInvolvementComponent {
{ id: 'education', label: 'Education' },
];

public ngAfterViewInit(): void {
// Initialize scroll state after view is ready
setTimeout(() => this.onScroll(), 0);
}

public readonly primaryMetrics = computed<OrganizationInvolvementMetricWithChart[]>((): OrganizationInvolvementMetricWithChart[] => {
const contributionsData = this.contributionsOverviewData();
const dashboardData = this.boardMemberDashboardData();
Expand Down Expand Up @@ -121,6 +129,18 @@ export class OrganizationInvolvementComponent {
container.scrollBy({ left: 300, behavior: 'smooth' });
}

public onScroll(): void {
if (!this.carouselScrollContainer?.nativeElement) return;
const container = this.carouselScrollContainer.nativeElement;

// Check if can scroll left (not at the start)
this.canScrollLeft.set(container.scrollLeft > 0);

// Check if can scroll right (not at the end)
const maxScrollLeft = container.scrollWidth - container.clientWidth;
this.canScrollRight.set(container.scrollLeft < maxScrollLeft - 1);
}

private initializeContributionsOverviewData() {
return toSignal(
this.selectedAccountId$.pipe(
Expand Down
Loading
Loading