Skip to content

Commit

Permalink
Merge pull request #38 from fleetbase/feature/countdown-component
Browse files Browse the repository at this point in the history
created base countdown timer component
  • Loading branch information
roncodes authored Jan 15, 2024
2 parents 1e237c8 + 4013367 commit 61f7f47
Show file tree
Hide file tree
Showing 11 changed files with 459 additions and 0 deletions.
3 changes: 3 additions & 0 deletions addon/components/countdown.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="countdown-container">
<p class={{this.remainingClass}}>{{this.remaining}}</p>
</div>
164 changes: 164 additions & 0 deletions addon/components/countdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { formatDuration, intervalToDuration } from 'date-fns';
import { isArray } from '@ember/array';
import { run } from '@ember/runloop';
import { computed } from '@ember/object';
export default class CountdownComponent extends Component {
/**
* An array that defines the units to display in the countdown.
*
* @memberof CountdownComponent
* @type {Array<string>}
* @default ['seconds']
*/
@tracked display = ['seconds'];

/**
* The remaining time in the countdown.
*
* @memberof CountdownComponent
* @type {string}
*/
@tracked remaining;

/**
* The duration of the countdown, specified in days, hours, minutes, and seconds.
*
* @memberof CountdownComponent
* @type {Object}
*/
@tracked duration = {};

/**
* The interval ID for the countdown timer.
*
* @memberof CountdownComponent
* @type {number}
*/
@tracked interval;

/**
* Creates an instance of CountdownComponent.
*
* @param {Object} owner - The owning object.
* @param {Object} options - Options for configuring the countdown.
* @param {Date} options.expiry - The expiration date for the countdown.
* @param {number} options.hours - The initial hours for the countdown.
* @param {number} options.minutes - The initial minutes for the countdown.
* @param {number} options.seconds - The initial seconds for the countdown.
* @param {number} options.days - The initial days for the countdown.
* @param {(string|Array<string>)} options.display - The units to display in the countdown.
*/
constructor(owner, { expiry, hours, minutes, seconds, days, display }) {
super(...arguments);

this.duration = {
days,
hours,
minutes,
seconds,
};

if (expiry instanceof Date) {
// use the date provided to set the hours minutes seconds
this.duration = intervalToDuration({ start: new Date(), end: expiry });
}

if (display) {
if (typeof display === 'string' && display.includes(',')) {
display = display.split(',');
}

if (isArray(display)) {
this.display = display;
}
}

this.startCountdown();
}

@computed('remaining')
get remainingClass() {
// Customize the threshold and class names as needed
if (this.remaining && this.durationToSeconds(this.duration) <= 5) {
return 'remaining-low'; // Add a CSS class for low time
} else {
return 'remaining-normal'; // Add a default CSS class
}
}

/**
* Starts the countdown timer.
*
* @memberof CountdownComponent
* @method
*/
startCountdown() {
this.interval = setInterval(() => {
run(() => {
let { duration } = this;

// if onlyDisplaySeconds === true
if (this.args.onlyDisplaySeconds === true) {
duration = {
seconds: this.durationToSeconds(this.duration),
};
}

this.remaining = formatDuration(duration);

// decrement seconds
duration.seconds--;

// set duration
if (duration.seconds < 0) {
duration.seconds = 0; // Stop the countdown at 0
clearInterval(this.interval);
}
});
}, 1000);
}

/**
* Converts the duration object to total seconds.
*
* @memberof CountdownComponent
* @method
* @param {Object} duration - The duration object.
* @returns {number} - The total seconds.
*/
durationToSeconds(duration) {
const { years = 0, months = 0, weeks = 0, days = 0, hours = 0, minutes = 0, seconds = 0 } = duration;
const totalSeconds = years * 365 * 24 * 60 * 60 + months * 30 * 24 * 60 * 60 + weeks * 7 * 24 * 60 * 60 + days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds;

return totalSeconds;
}

/**
* Restarts the countdown by resetting the timeRemaining property and clearing the interval.
*
* @method restartCountdown
*/
restartCountdown() {
clearInterval(this.interval);
// Reset properties
this.remaining = null;
this.duration = {
days: this.args.days || 0,
hours: this.args.hours || 0,
minutes: this.args.minutes || 0,
seconds: this.args.seconds || 0,
};
this.startCountdown();
}

/**
* Cleans up the interval when the component is being destroyed.
* @method willDestroy
*/
willDestroy() {
super.willDestroy(...arguments);
clearInterval(this.interval);
}
}
13 changes: 13 additions & 0 deletions addon/components/otp-input.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="otp-input-container flex space-x-2">
{{#each this.otpValues as |value index|}}
<input
type="text"
id={{concat "otp-input-" index}}
value={{value}}
maxlength="1"
{{on "input" (fn this.handleInput index)}}
{{on "focus" (fn this.handleFocus index)}}
{{on "keydown" (fn this.handleKeyDown index)}}
/>
{{/each}}
</div>
139 changes: 139 additions & 0 deletions addon/components/otp-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// app/components/otp-input.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';

/**
* Glimmer component for handling OTP (One-Time Password) input.
*
* @class OtpInputComponent
* @extends Component
*/
export default class OtpInputComponent extends Component {
numberOfDigits = 6;

/**
* Array to track individual digit values of the OTP.
*
* @property {Array} otpValues
* @default ['', '', '', '', '', '']
* @tracked
*/
@tracked otpValues;

/**
* Tracked property for handling the OTP value passed from the parent.
*
* @property {String} value
* @tracked
*/
@tracked value;

/**
* Constructor for the OTP input component.
*
* @constructor
*/
constructor() {
super(...arguments);
this.otpValues = Array.from({ length: this.numberOfDigits }, () => '');
this.handleInput = this.handleInput.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}

/**
* Getter for the complete OTP value obtained by joining individual digits.
*
* @property {String} otpValue
*/
get otpValue() {
return this.otpValues.join('');
}

/**
* Setter for updating the OTP value based on user input.
*
* @property {String} otpValue
*/
set otpValue(newValue) {
if (typeof newValue === 'string') {
this.otpValues = newValue.split('').slice(0, this.numberOfDigits);
}
}

/**
* Handles focus on the input field at a specified index.
*
* @method handleFocus
* @param {Number} index - The index of the input field to focus on.
*/
handleFocus(index) {
const inputId = `otp-input-${index}`;
const inputElement = document.getElementById(inputId);

if (inputElement) {
inputElement.focus();
}
}

/**
* Handles input events on the input field at a specified index.
*
* @method handleInput
* @param {Number} index - The index of the input field being edited.
* @param {Event} event - The input event object.
*/
handleInput(index, event) {
if (!event || !event.target) {
console.error('Invalid event object in handleInput');
return;
}

const inputValue = event.target.value;

this.otpValues[index] = inputValue;

if (inputValue === '' && index > 0) {
this.handleFocus(index - 1);
} else if (index < this.numberOfDigits - 1) {
this.handleFocus(index + 1);
}

if (this.otpValues.every(value => value !== '')) {
const completeOtpValue = this.otpValues.join('');
this.args.onInput(completeOtpValue);
}
}

/**
* Handles keydown events on the input field at a specified index.
*
* @method handleKeyDown
* @param {Number} index - The index of the input field.
* @param {Event} event - The keydown event object.
*/
handleKeyDown(index, event) {
switch (event.key) {
case 'ArrowLeft':
if (index > 0) {
this.handleFocus(index - 1);
}
break;
case 'ArrowRight':
if (index < this.numberOfDigits - 1) {
this.handleFocus(index + 1);
}
break;
case 'Backspace':
if (this.otpValues[index] !== '') {
this.otpValues[index] = '';
} else if (index > 0) {
this.handleFocus(index - 1);
}
break;
default:
break;
}
}
}
2 changes: 2 additions & 0 deletions addon/styles/addon.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
@import 'components/drawer.css';
@import 'components/full-calendar.css';
@import 'components/sidebar-toggle.css';
@import 'components/countdown.css';
@import 'components/otp-input.css';

/** Third party */
@import 'air-datepicker/air-datepicker.css';
55 changes: 55 additions & 0 deletions addon/styles/components/countdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.countdown-container {
display: flex;
flex-direction: column;
align-items: center;
}

.remaining-normal {
font-size: 24px;
color: #3498db;
}

.remaining-low {
font-size: 24px;
color: #e74c3c;
font-weight: bold;
animation: pulse 0.5s infinite alternate;
}

.progress-ring {
margin-top: 10px;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: transparent;
border: 5px solid #3498db;
animation: rotate 1s linear infinite;
}

.progress-circle {
width: 100%;
height: 100%;
border-radius: 50%;
clip: rect(0, 60px, 60px, 30px);
background-color: #fff;
transform: rotate(90deg);
transform-origin: 50% 50%;
}

@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

@keyframes pulse {
from {
transform: scale(1);
}
to {
transform: scale(1.1);
}
}
Loading

0 comments on commit 61f7f47

Please sign in to comment.