Skip to content

Commit 6f9ae54

Browse files
committed
First release
0 parents  commit 6f9ae54

14 files changed

+450
-0
lines changed

README.md

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# NgCircleTimer
2+
3+
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
4+
5+
## Code scaffolding
6+
7+
Run `ng generate component component-name --project ng-circle-timer` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project ng-circle-timer`.
8+
> Note: Don't forget to add `--project ng-circle-timer` or else it will be added to the default project in your `angular.json` file.
9+
10+
## Build
11+
12+
Run `ng build ng-circle-timer` to build the project. The build artifacts will be stored in the `dist/` directory.
13+
14+
## Publishing
15+
16+
After building your library with `ng build ng-circle-timer`, go to the dist folder `cd dist/ng-circle-timer` and run `npm publish`.
17+
18+
## Running unit tests
19+
20+
Run `ng test ng-circle-timer` to execute the unit tests via [Karma](https://karma-runner.github.io).
21+
22+
## Further help
23+
24+
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

karma.conf.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Karma configuration file, see link for more information
2+
// https://karma-runner.github.io/1.0/config/configuration-file.html
3+
4+
module.exports = function (config) {
5+
config.set({
6+
basePath: '',
7+
frameworks: ['jasmine', '@angular-devkit/build-angular'],
8+
plugins: [
9+
require('karma-jasmine'),
10+
require('karma-chrome-launcher'),
11+
require('karma-jasmine-html-reporter'),
12+
require('karma-coverage'),
13+
require('@angular-devkit/build-angular/plugins/karma')
14+
],
15+
client: {
16+
jasmine: {
17+
// you can add configuration options for Jasmine here
18+
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
19+
// for example, you can disable the random execution with `random: false`
20+
// or set a specific seed with `seed: 4321`
21+
},
22+
clearContext: false // leave Jasmine Spec Runner output visible in browser
23+
},
24+
jasmineHtmlReporter: {
25+
suppressAll: true // removes the duplicated traces
26+
},
27+
coverageReporter: {
28+
dir: require('path').join(__dirname, '../../coverage/ng-circle-timer'),
29+
subdir: '.',
30+
reporters: [
31+
{ type: 'html' },
32+
{ type: 'text-summary' }
33+
]
34+
},
35+
reporters: ['progress', 'kjhtml'],
36+
port: 9876,
37+
colors: true,
38+
logLevel: config.LOG_INFO,
39+
autoWatch: true,
40+
browsers: ['Chrome'],
41+
singleRun: false,
42+
restartOnFileChange: true
43+
});
44+
};

ng-package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3+
"dest": "../../dist/ng-circle-timer",
4+
"lib": {
5+
"entryFile": "src/public-api.ts"
6+
}
7+
}

package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "ng-circle-timer",
3+
"version": "1.0.1",
4+
"description": "Simple circle countdown timer component for Angular.",
5+
"repository": {
6+
"type": "git",
7+
"url": "git+https://github.com/squareetlabs/ng-circle-timer.git"
8+
},
9+
"license": "MIT",
10+
"homepage": "https://github.com/squareetlabs/ng-circle-timer",
11+
"keywords": [
12+
"circle countdown timer",
13+
"countdown timer",
14+
"circle timer",
15+
"circle countdown",
16+
"squareetlabs"
17+
],
18+
"author": "Jacobo Cantorna Cigarrán <[email protected]> (https://squareet.com)",
19+
"contributors": [
20+
"Alberto Rial Barreiro <[email protected]> (https://squareet.com)"
21+
],
22+
"peerDependencies": {
23+
"@angular/common": "^12.2.0",
24+
"@angular/core": "^12.2.0"
25+
},
26+
"dependencies": {
27+
"tslib": "^2.3.0"
28+
},
29+
"bugs": {
30+
"url": "https://github.com/squareetlabs/ng-circle-timer/issues"
31+
},
32+
"main": "src/public-api.ts",
33+
"scripts": {
34+
"test": "npm run test"
35+
}
36+
}
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { NgCircleTimerComponent } from './ng-circle-timer.component';
4+
5+
describe('NgCircleTimerComponent', () => {
6+
let component: NgCircleTimerComponent;
7+
let fixture: ComponentFixture<NgCircleTimerComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [ NgCircleTimerComponent ]
12+
})
13+
.compileComponents();
14+
});
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(NgCircleTimerComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should create', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});

src/lib/ng-circle-timer.component.ts

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
2+
import { Subject, timer } from 'rxjs';
3+
import { takeUntil } from 'rxjs/operators';
4+
export type StartDate = Date | string | number;
5+
@Component({
6+
selector: 'ng-circle-timer',
7+
template: `
8+
<div class="base-timer">
9+
<svg class="base-timer-svg"
10+
viewBox="0 0 100 100"
11+
xmlns="http://www.w3.org/2000/svg">
12+
<g class="base-timer-circle">
13+
<circle [class.base-timer-completed]=completed [class.base-timer-stroke]=!completed
14+
cx="50"
15+
cy="50"
16+
r="45" />
17+
<path [attr.stroke-dasharray]="dasharray"
18+
[style.stroke]="color"
19+
id="remaining-time-stroke"
20+
d=" M 50, 50
21+
m -45, 0
22+
a 45,45 0 1,0 90,0
23+
a 45,45 0 1,0 -90,0">
24+
</path>
25+
</g>
26+
</svg>
27+
<span class="time">
28+
<span>{{formattedTimeLeft}}</span>
29+
</span>
30+
</div>
31+
`,
32+
styles: [
33+
]
34+
})
35+
export class NgCircleTimerComponent implements OnInit, OnDestroy {
36+
@Input() startDate?: string; // only to set initial state (ngOnInit)
37+
@Input() duration = 0; // milliseconds
38+
@Input() color = '#1cbbf8';
39+
40+
@Output() onComplete = new EventEmitter<boolean>();
41+
42+
destroy$ = new Subject<void>();
43+
44+
timeLeft = 0;
45+
ticking = false;
46+
completed = false;
47+
formattedTimeLeft = '';
48+
49+
readonly fullDasharray = 283;
50+
dasharray = `${this.fullDasharray} ${this.fullDasharray}`;
51+
52+
constructor() {}
53+
54+
ngOnInit() {
55+
this.init(this.startDate);
56+
}
57+
58+
ngOnDestroy() {
59+
this.destroy$.next();
60+
this.destroy$.complete();
61+
}
62+
63+
init(startDate?: StartDate): void {
64+
this.setTimeLeft(startDate);
65+
this.formatTimeLeft();
66+
this.setDasharray();
67+
68+
if (this.timeLeft === 0) {
69+
this.completed = true;
70+
}
71+
}
72+
73+
setTimeLeft(startDate?: StartDate): number {
74+
this.ticking = false;
75+
this.completed = false;
76+
this.destroy$.next();
77+
78+
if (!startDate) {
79+
this.timeLeft = this.duration;
80+
return 0;
81+
}
82+
83+
const startTime = new Date(<string>startDate).getTime();
84+
const endTime = startTime + this.duration;
85+
const timeLeftRaw = endTime - Date.now();
86+
if (timeLeftRaw <= 0) {
87+
this.timeLeft = 0;
88+
return 0;
89+
}
90+
91+
const timeLeftSeconds = timeLeftRaw / 1000;
92+
93+
this.timeLeft = Math.floor(timeLeftSeconds) * 1000;
94+
return timeLeftSeconds % 1;
95+
}
96+
97+
start(startDate?: StartDate, delayMs = 0, replaying = false): void {
98+
if (this.ticking) {
99+
console.log('Cannot start: timer already running.');
100+
return;
101+
}
102+
103+
const decimalPortion = this.setTimeLeft(startDate);
104+
const startDelayMs = delayMs + decimalPortion * 1000;
105+
this.formatTimeLeft();
106+
this.setDasharray();
107+
108+
if (this.timeLeft === 0) {
109+
this.completed = true;
110+
console.log('Cannot start: timer already completed.');
111+
return;
112+
}
113+
114+
timer(startDelayMs, 1000)
115+
.pipe(takeUntil(this.destroy$))
116+
.subscribe(() => {
117+
this.ticking = true;
118+
this.timeLeft -= 1000;
119+
if (this.timeLeft <= 0) {
120+
this.timeLeft = 0;
121+
this.ticking = false;
122+
this.completed = true;
123+
this.onComplete.emit(replaying);
124+
this.destroy$.next();
125+
}
126+
127+
this.formatTimeLeft();
128+
this.setDasharray();
129+
});
130+
}
131+
132+
replay(startDate?: StartDate): void {
133+
this.start(startDate, 1200, true);
134+
}
135+
136+
pause(): void {}
137+
138+
continue(): void {}
139+
140+
complete(): void {
141+
this.timeLeft = 0;
142+
this.ticking = false;
143+
this.completed = true;
144+
this.destroy$.next();
145+
146+
this.formatTimeLeft();
147+
this.setDasharray();
148+
}
149+
150+
isTicking(): boolean {
151+
return this.ticking;
152+
}
153+
154+
isCompleted(): boolean {
155+
return this.completed;
156+
}
157+
158+
formatTimeLeft(): void {
159+
if (this.timeLeft <= 0) {
160+
this.timeLeft = 0;
161+
}
162+
163+
const daysLeft = Math.floor(this.timeLeft / (1000 * 60 * 60 * 24));
164+
const hoursLeft = Math.floor((this.timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
165+
const minutesLeft = Math.floor((this.timeLeft % (1000 * 60 * 60)) / (1000 * 60));
166+
const secondsLeft = Math.floor((this.timeLeft % (1000 * 60)) / 1000);
167+
168+
const formattedDays = daysLeft < 10 ? `0${daysLeft}` : `${daysLeft}`;
169+
const formattedHours = hoursLeft < 10 ? `0${hoursLeft}` : `${hoursLeft}`;
170+
const formattedMinutes = minutesLeft < 10 ? `0${minutesLeft}` : `${minutesLeft}`;
171+
const formattedSeconds = secondsLeft < 10 ? `0${secondsLeft}` : `${secondsLeft}`;
172+
173+
this.formattedTimeLeft = `${formattedMinutes}:${formattedSeconds}`;
174+
175+
if (formattedHours !== '00' || formattedDays !== '00') {
176+
this.formattedTimeLeft = `${formattedHours}:` + this.formattedTimeLeft;
177+
}
178+
179+
if (formattedDays !== '00') {
180+
this.formattedTimeLeft = `${formattedDays}:` + this.formattedTimeLeft;
181+
}
182+
}
183+
184+
setDasharray(): void {
185+
const rawFraction = this.timeLeft / this.duration;
186+
const fraction = rawFraction - (1 / this.duration) * (1 - rawFraction);
187+
const remaining = Math.round(fraction * this.fullDasharray);
188+
189+
this.dasharray = `${remaining} ${this.fullDasharray}`;
190+
}
191+
}

src/lib/ng-circle-timer.module.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { NgModule } from '@angular/core';
2+
import { NgCircleTimerComponent } from './ng-circle-timer.component';
3+
4+
5+
6+
@NgModule({
7+
declarations: [
8+
NgCircleTimerComponent
9+
],
10+
imports: [
11+
],
12+
exports: [
13+
NgCircleTimerComponent
14+
]
15+
})
16+
export class NgCircleTimerModule { }
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { NgCircleTimerService } from './ng-circle-timer.service';
4+
5+
describe('NgCircleTimerService', () => {
6+
let service: NgCircleTimerService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(NgCircleTimerService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});

src/lib/ng-circle-timer.service.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Injectable } from '@angular/core';
2+
3+
@Injectable({
4+
providedIn: 'root'
5+
})
6+
export class NgCircleTimerService {
7+
8+
constructor() { }
9+
}

src/public-api.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
* Public API Surface of ng-circle-timer
3+
*/
4+
5+
export * from './lib/ng-circle-timer.service';
6+
export * from './lib/ng-circle-timer.component';
7+
export * from './lib/ng-circle-timer.module';

0 commit comments

Comments
 (0)