From c14e50e6de61b46626064f4bad04907914b977ac Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Thu, 14 Nov 2024 14:26:07 +0100 Subject: [PATCH] feat: add ProgressBar component with customizable properties and styles --- .../components/ProgressBar/ProgressBar.scss | 66 +++++++++++++++++++ .../ProgressBar/ProgressBar.test.tsx | 62 +++++++++++++++++ .../components/ProgressBar/ProgressBar.tsx | 56 ++++++++++++++++ frontend/src/stories/ProgressBar.stories.tsx | 28 ++++++++ 4 files changed, 212 insertions(+) create mode 100644 frontend/src/components/ProgressBar/ProgressBar.scss create mode 100644 frontend/src/components/ProgressBar/ProgressBar.test.tsx create mode 100644 frontend/src/components/ProgressBar/ProgressBar.tsx create mode 100644 frontend/src/stories/ProgressBar.stories.tsx diff --git a/frontend/src/components/ProgressBar/ProgressBar.scss b/frontend/src/components/ProgressBar/ProgressBar.scss new file mode 100644 index 000000000..52062ba6b --- /dev/null +++ b/frontend/src/components/ProgressBar/ProgressBar.scss @@ -0,0 +1,66 @@ +// import variables.scss +@import '../../scss/variables.scss'; + +// Progress bar variables +$progress-height-sm: 20px; // Increased to accommodate text +$progress-height-md: 24px; // Increased to accommodate text +$progress-height-lg: 32px; // Increased to accommodate text +$progress-border-radius: .5rem; +$progress-background: $gray-900; +$progress-fill-color: $pink; +$transition-duration: 0.3s; + +.aml__progress-bar { + width: 100%; + position: relative; + + &-container { + background-color: $progress-background; + border-radius: $progress-border-radius; + border: 1px solid $gray-200; + overflow: hidden; + width: 100%; + height: $progress-height-md; + position: relative; + } + + &-fill { + background-color: $progress-fill-color; + height: 100%; + border-radius: $progress-border-radius; + transition: width $transition-duration ease-in-out; + position: absolute; + top: 0; + left: 0; + } + + &-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + padding: 0 10px; + z-index: 1; + pointer-events: none; + color: #fff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + font-size: 14px; + + &.size-sm { + font-size: 12px; + } + + &.size-md { + font-size: 14px; + } + + &.size-lg { + font-size: 16px; + } + } +} diff --git a/frontend/src/components/ProgressBar/ProgressBar.test.tsx b/frontend/src/components/ProgressBar/ProgressBar.test.tsx new file mode 100644 index 000000000..0aa52db86 --- /dev/null +++ b/frontend/src/components/ProgressBar/ProgressBar.test.tsx @@ -0,0 +1,62 @@ +import { render } from '@testing-library/react'; +import ProgressBar from './ProgressBar'; +import { describe, it, expect } from 'vitest'; + +describe('ProgressBar', () => { + it('renders correctly with default props', () => { + const { container } = render(); + + const progressBar = container.querySelector('.aml__progress-bar'); + const progressFill = container.querySelector('.aml__progress-bar-fill'); + const content = container.querySelector('.aml__progress-bar-content'); + + expect(progressBar).toBeTruthy(); + expect(progressFill).toBeTruthy(); + expect(content).toBeTruthy(); + expect(progressFill?.getAttribute('style')).toBe('width: 50%;'); + }); + + it('clamps values to be between 0 and max', () => { + const { container: containerNegative } = render(); + const { container: containerOverMax } = render(); + + const fillNegative = containerNegative.querySelector('.aml__progress-bar-fill'); + const fillOverMax = containerOverMax.querySelector('.aml__progress-bar-fill'); + + expect(fillNegative?.getAttribute('style')).toBe('width: 0%;'); + expect(fillOverMax?.getAttribute('style')).toBe('width: 100%;'); + }); + + it('displays label when provided', () => { + const { getByText } = render(); + expect(getByText('Loading...')).toBeTruthy(); + }); + + it('does not show percentage by default', () => { + const { queryByText } = render(); + + const percentageElement = queryByText('75%'); + expect(percentageElement).toBeNull(); + }); + + it('hides percentage when showPercentage is false', () => { + const { container } = render(); + const percentageElement = container.querySelector('.aml__progress-bar-content-percentage'); + expect(percentageElement).toBeFalsy(); + }); + + it('calculates percentage correctly with custom max value when showPercentage is true', () => { + const { container, getByText } = render(); + const progressFill = container.querySelector('.aml__progress-bar-fill'); + + expect(progressFill?.getAttribute('style')).toBe('width: 75%;'); + expect(getByText('75%')).toBeTruthy(); + }); + + it('displays both label and percentage when both are provided', () => { + const { getByText } = render(); + + expect(getByText('Loading...')).toBeTruthy(); + expect(getByText('50%')).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/ProgressBar/ProgressBar.tsx b/frontend/src/components/ProgressBar/ProgressBar.tsx new file mode 100644 index 000000000..9e9da0333 --- /dev/null +++ b/frontend/src/components/ProgressBar/ProgressBar.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import './ProgressBar.scss'; + +interface ProgressBarProps { + /** + * Current progress value (0-100) + */ + value: number; + /** + * Maximum value (defaults to 100) + */ + max?: number; + /** + * Show percentage text (defaults to true) + */ + showPercentage?: boolean; + /** + * Optional label text to display above progress bar + */ + label?: string; +} + +const ProgressBar: React.FC = ({ + value, + max = 100, + showPercentage = false, + label, +}) => { + const clampedValue = Math.min(Math.max(0, value), max); + const percentage = Math.round((clampedValue / max) * 100); + + return ( +
+
+
+
+ {label && ( + + {label} + + )} + {showPercentage && ( + + {percentage}% + + )} +
+
+
+ ); +}; + +export default ProgressBar; diff --git a/frontend/src/stories/ProgressBar.stories.tsx b/frontend/src/stories/ProgressBar.stories.tsx new file mode 100644 index 000000000..b57d83a4a --- /dev/null +++ b/frontend/src/stories/ProgressBar.stories.tsx @@ -0,0 +1,28 @@ +import ProgressBar from "../components/ProgressBar/ProgressBar"; + +export default { + title: "ProgressBar", + component: ProgressBar, + parameters: { + layout: "fullscreen", + }, +}; + +export const Default = { + args: { + value: 50, + max: 100, + showPercentage: false, + label: "3 / 20", + size: "md", + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +};