-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #38 from fleetbase/feature/countdown-component
created base countdown timer component
- Loading branch information
Showing
11 changed files
with
459 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,3 @@ | ||
<div class="countdown-container"> | ||
<p class={{this.remainingClass}}>{{this.remaining}}</p> | ||
</div> |
This file contains 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,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); | ||
} | ||
} |
This file contains 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,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> |
This file contains 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,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; | ||
} | ||
} | ||
} |
This file contains 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 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,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); | ||
} | ||
} |
Oops, something went wrong.