unictive-angular 1.0.28 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/utils/resource/src/app/API/README.md +10 -0
- package/bin/utils/resource/src/app/MODULES/authentication/login/login.component.html +20 -0
- package/bin/utils/resource/src/app/MODULES/authentication/login/login.component.scss +0 -0
- package/bin/utils/resource/src/app/MODULES/authentication/login/login.component.ts +35 -0
- package/bin/utils/resource/src/app/MODULES/authentication/login/login.module.ts +19 -0
- package/bin/utils/resource/src/app/MODULES/authentication/page404/page404.component.html +3 -0
- package/bin/utils/resource/src/app/MODULES/authentication/page404/page404.component.scss +0 -0
- package/bin/utils/resource/src/app/MODULES/authentication/page404/page404.component.ts +16 -0
- package/bin/utils/resource/src/app/MODULES/authentication/page404/page404.module.ts +17 -0
- package/bin/utils/resource/src/app/MODULES/authentication/register/register.component.html +3 -0
- package/bin/utils/resource/src/app/MODULES/authentication/register/register.component.scss +0 -0
- package/bin/utils/resource/src/app/MODULES/authentication/register/register.component.ts +16 -0
- package/bin/utils/resource/src/app/MODULES/authentication/register/register.module.ts +18 -0
- package/bin/utils/resource/src/app/MODULES/home/home.component.html +4 -0
- package/bin/utils/resource/src/app/MODULES/home/home.component.scss +0 -0
- package/bin/utils/resource/src/app/MODULES/home/home.component.ts +16 -0
- package/bin/utils/resource/src/app/MODULES/home/home.module.ts +19 -0
- package/bin/utils/resource/src/app/app.html +0 -262
- package/bin/utils/resource/src/app/app.routes.ts +20 -1
- package/bin/utils/resource/src/app/core/config/entity-store.ts +69 -0
- package/bin/utils/resource/src/app/core/config/single-store.ts +51 -0
- package/bin/utils/resource/src/app/core/models/entities/screen.entity.spec.ts +41 -0
- package/bin/utils/resource/src/app/core/models/entities/screen.entity.ts +16 -0
- package/bin/utils/resource/src/app/core/respositories/screen_repo/screen.repo.ts +19 -0
- package/bin/utils/resource/src/app/core/services/helper.service.ts +238 -0
- package/bin/utils/resource/src/app/core/services/listener.service.ts +15 -0
- package/bin/utils/resource/src/app/shared/libs/library.module.ts +14 -0
- package/bin/utils/resource/src/app/shared/ui/input-text/input-text.component.scss +0 -0
- package/bin/utils/resource/src/app/shared/ui/input-text/input-text.component.ts +64 -0
- package/bin/utils/resource/src/app/shared/utils/validator_pattern.ts +25 -0
- package/bin/utils/resource/src/app/shared/widgets/alert-error-input/alert-error-input.component.html +94 -0
- package/bin/utils/resource/src/app/shared/widgets/alert-error-input/alert-error-input.component.ts +24 -0
- package/bin/utils/resource/src/app/shared/widgets/widget_game/widget_game.component.html +20 -0
- package/bin/utils/resource/src/app/shared/widgets/widget_game/widget_game.component.scss +0 -0
- package/bin/utils/resource/src/app/shared/widgets/widget_game/widget_game.component.ts +65 -0
- package/bin/utils/resource/src/app/shared/widgets/widget_game/widget_game.module.ts +12 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { RepoScreen } from '@core/respository/screen_repo/screen.repo';
|
|
2
|
+
import { ScreenEntity, Screen_m } from './screen.entity';
|
|
3
|
+
import { provideZonelessChangeDetection } from '@angular/core';
|
|
4
|
+
import { TestBed } from '@angular/core/testing';
|
|
5
|
+
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
describe('ScreenEntity', () => {
|
|
9
|
+
let repo: RepoScreen;
|
|
10
|
+
|
|
11
|
+
const mockScreen: Screen_m = {
|
|
12
|
+
timestamp: Date.now(),
|
|
13
|
+
type: 'test',
|
|
14
|
+
id: 123,
|
|
15
|
+
name: 'Mock Screen',
|
|
16
|
+
mode: 'demo',
|
|
17
|
+
time: 60000
|
|
18
|
+
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
await TestBed.configureTestingModule({
|
|
23
|
+
imports: [HttpClientTestingModule],
|
|
24
|
+
providers: [provideZonelessChangeDetection()]
|
|
25
|
+
}).compileComponents();
|
|
26
|
+
|
|
27
|
+
repo = TestBed.inject(RepoScreen);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should set and get screen', () => {
|
|
31
|
+
repo.setData(mockScreen);
|
|
32
|
+
expect(repo.getData.value).toEqual(mockScreen);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should update state when entity is set again', () => {
|
|
36
|
+
const newMock: Screen_m = { ...mockScreen, id: 456, name: 'New Mock' };
|
|
37
|
+
repo.setData(newMock);
|
|
38
|
+
expect(repo.getData.value?.id).toBe(456);
|
|
39
|
+
expect(repo.getData.value?.name).toBe('New Mock');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// import { entityStoreFactory } from "@core/config/entity-store";
|
|
2
|
+
import { singleStoreFactory } from "@core/config/single-store";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export interface Screen_m {
|
|
7
|
+
timestamp: number;
|
|
8
|
+
type: string;
|
|
9
|
+
id: number;
|
|
10
|
+
name: string;
|
|
11
|
+
mode: string;
|
|
12
|
+
time: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const ScreenEntity = singleStoreFactory<Screen_m>();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { ScreenEntity } from '@core/models/entities/screen.entity';
|
|
4
|
+
import { logger } from '@shared/utils/logger';
|
|
5
|
+
|
|
6
|
+
@Injectable({ providedIn: 'root' })
|
|
7
|
+
export class RepoScreen {
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
constructor() { }
|
|
11
|
+
|
|
12
|
+
setData(screen: any) {
|
|
13
|
+
ScreenEntity.set(screen);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get getData() {
|
|
17
|
+
return ScreenEntity.state();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { computed, Injectable, signal } from '@angular/core';
|
|
2
|
+
import { StorageMap } from '@ngx-pwa/local-storage';
|
|
3
|
+
import { ListenerService } from './listener.service';
|
|
4
|
+
import { lastValueFrom } from 'rxjs';
|
|
5
|
+
import { Router } from '@angular/router';
|
|
6
|
+
import Swal from 'sweetalert2';
|
|
7
|
+
import { ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
|
|
8
|
+
|
|
9
|
+
export interface Popup_m {
|
|
10
|
+
type:
|
|
11
|
+
| 'delete'
|
|
12
|
+
| 'success'
|
|
13
|
+
| 'error'
|
|
14
|
+
| 'warning'
|
|
15
|
+
| 'info'
|
|
16
|
+
| 'confirm'
|
|
17
|
+
| 'suspend'
|
|
18
|
+
| 'change'
|
|
19
|
+
| 'send'
|
|
20
|
+
| 'restore';
|
|
21
|
+
icon?: string;
|
|
22
|
+
title: string;
|
|
23
|
+
message: string;
|
|
24
|
+
button: string;
|
|
25
|
+
button_cancel?: string;
|
|
26
|
+
showCancel?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@Injectable({
|
|
31
|
+
providedIn: 'root'
|
|
32
|
+
})
|
|
33
|
+
export class HelperService {
|
|
34
|
+
|
|
35
|
+
private width = signal(window.innerWidth);
|
|
36
|
+
screenWidth = this.width.asReadonly();
|
|
37
|
+
|
|
38
|
+
constructor(
|
|
39
|
+
private storage: StorageMap,
|
|
40
|
+
private listener: ListenerService,
|
|
41
|
+
private router: Router
|
|
42
|
+
) {
|
|
43
|
+
window.addEventListener('resize', () => {
|
|
44
|
+
this.width.set(window.innerWidth);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
screenSize = computed<'mobile' | 'tablet' | 'desktop'>(() => {
|
|
49
|
+
const w = this.width();
|
|
50
|
+
if (w < 768) return 'mobile';
|
|
51
|
+
if (w < 1024) return 'tablet';
|
|
52
|
+
return 'desktop';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
getTimeInZone(timeZone: any) {
|
|
57
|
+
const options: any = {
|
|
58
|
+
timeZone: timeZone,
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: '2-digit',
|
|
61
|
+
day: '2-digit',
|
|
62
|
+
hour: '2-digit',
|
|
63
|
+
minute: '2-digit',
|
|
64
|
+
second: '2-digit',
|
|
65
|
+
hour12: false,
|
|
66
|
+
};
|
|
67
|
+
const date = new Date();
|
|
68
|
+
const formatter = new Intl.DateTimeFormat('en-US', options);
|
|
69
|
+
const parts = formatter.formatToParts(date);
|
|
70
|
+
const dateParts: any = {};
|
|
71
|
+
parts.forEach((part) => {
|
|
72
|
+
if (part.type !== 'literal') {
|
|
73
|
+
dateParts[part.type] = part.value;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return `${dateParts.year}-${dateParts.month}-${dateParts.day} ${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//Fix time global
|
|
81
|
+
getTimeNow() {
|
|
82
|
+
// Waktu di zona waktu Indonesia Barat (WIB)
|
|
83
|
+
const waktuWIB = this.getTimeInZone('Asia/Jakarta');
|
|
84
|
+
|
|
85
|
+
//console.log(waktuWIB);
|
|
86
|
+
|
|
87
|
+
// Waktu di zona waktu Indonesia Tengah (WITA)
|
|
88
|
+
const waktuWITA = this.getTimeInZone('Asia/Makassar');
|
|
89
|
+
//console.log('Waktu WITA (Asia/Makassar):', waktuWITA);
|
|
90
|
+
|
|
91
|
+
// Waktu di zona waktu Indonesia Timur (WIT)
|
|
92
|
+
const waktuWIT = this.getTimeInZone('Asia/Jayapura');
|
|
93
|
+
//console.log('Waktu WIT (Asia/Jayapura):', waktuWIT);
|
|
94
|
+
|
|
95
|
+
return new Date(waktuWIB);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Realtime screen size detection
|
|
100
|
+
* cara menggunakan isMobile(), isTablet(), isDesktop()
|
|
101
|
+
*/
|
|
102
|
+
isMobile = computed(() => this.screenSize() === 'mobile');
|
|
103
|
+
isTablet = computed(() => this.screenSize() === 'tablet');
|
|
104
|
+
isDesktop = computed(() => this.screenSize() === 'desktop');
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Download file from Blob data.
|
|
108
|
+
* @param data - Blob data of the file.
|
|
109
|
+
* @param filename - Name of the file to be downloaded.
|
|
110
|
+
*/
|
|
111
|
+
//MARK: DOWNLOAD FILE
|
|
112
|
+
downloadFile(data: any, filename: string) {
|
|
113
|
+
const url = window.URL.createObjectURL(data);
|
|
114
|
+
const a = document.createElement('a');
|
|
115
|
+
a.href = url;
|
|
116
|
+
a.download = filename;
|
|
117
|
+
a.click();
|
|
118
|
+
window.URL.revokeObjectURL(url);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//MARK: STORAGE
|
|
122
|
+
setStorage(key: string, value: any) {
|
|
123
|
+
return lastValueFrom(this.storage.set(key, value));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getStorage(key: string) {
|
|
127
|
+
return lastValueFrom(this.storage.get(key));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
removeStorage(key: string) {
|
|
131
|
+
return lastValueFrom(this.storage.delete(key));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Menampilkan file dalam tab baru atau sebagai iframe.
|
|
136
|
+
* @param file - File atau URL string yang akan ditampilkan.
|
|
137
|
+
*/
|
|
138
|
+
//MARK: SHOW FILE
|
|
139
|
+
showFile(file: File | string) {
|
|
140
|
+
if (typeof file === 'string') {
|
|
141
|
+
const newTab = window.open(file, '_blank');
|
|
142
|
+
if (!newTab) {
|
|
143
|
+
this.showPopup('error', 'Maaf', 'Unable to open file.');
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
let reader = new FileReader();
|
|
147
|
+
reader.readAsDataURL(file);
|
|
148
|
+
reader.onload = (e) => {
|
|
149
|
+
if (e.target?.result) {
|
|
150
|
+
const newTab = window.open();
|
|
151
|
+
if (newTab) {
|
|
152
|
+
newTab.document.body.innerHTML = `<iframe src="${e.target.result}" style="width:100%; height:100%;" frameborder="0"></iframe>`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async showLoading(message?: any) {
|
|
160
|
+
return await this.listener.$loading.set({ load: true, message: message });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
dismissLoading() {
|
|
164
|
+
this.listener.$loading.set({ load: false, message: null });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
routerLink(link: string) {
|
|
168
|
+
this.router.navigateByUrl(link);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//--------------------------- POP UP-------------------------//
|
|
172
|
+
showPopup(icon: any, title: any, msg: any, timer: boolean = false) {
|
|
173
|
+
Swal.fire({
|
|
174
|
+
title: title,
|
|
175
|
+
html: msg,
|
|
176
|
+
timer: timer ? 3000 : 0,
|
|
177
|
+
icon: icon,
|
|
178
|
+
allowOutsideClick: false,
|
|
179
|
+
heightAuto: false,
|
|
180
|
+
allowEscapeKey: false,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
dismissPopup() {
|
|
185
|
+
Swal.close();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async confirmationAlert(data: Popup_m): Promise<boolean> {
|
|
189
|
+
switch (data.type) {
|
|
190
|
+
case 'delete':
|
|
191
|
+
data.icon = 'icon-delete.svg';
|
|
192
|
+
break;
|
|
193
|
+
case 'success':
|
|
194
|
+
data.icon = 'icon-success.svg';
|
|
195
|
+
break;
|
|
196
|
+
case 'confirm':
|
|
197
|
+
data.icon = 'icon-confirm.svg';
|
|
198
|
+
break;
|
|
199
|
+
case 'suspend':
|
|
200
|
+
data.icon = 'icon-suspend.svg';
|
|
201
|
+
break;
|
|
202
|
+
case 'change':
|
|
203
|
+
data.icon = 'icon-edit.svg';
|
|
204
|
+
break;
|
|
205
|
+
case 'send':
|
|
206
|
+
data.icon = 'icon-send.png';
|
|
207
|
+
break;
|
|
208
|
+
case 'restore':
|
|
209
|
+
data.icon = 'icon-restore.png';
|
|
210
|
+
break;
|
|
211
|
+
default:
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return Swal.fire({
|
|
216
|
+
html: `
|
|
217
|
+
<h4 class="text-[18px] lg:text-[24px] mb-[16px] text-[#272727]">${data.title}</h4>
|
|
218
|
+
<div class="!text-[#272727] text-[16px] leading-[1.2]">${data.message}</div>
|
|
219
|
+
`,
|
|
220
|
+
|
|
221
|
+
showCancelButton: data?.showCancel ?? true,
|
|
222
|
+
confirmButtonText: data.button,
|
|
223
|
+
cancelButtonText: data?.button_cancel ? data.button_cancel : 'Batal',
|
|
224
|
+
reverseButtons: true,
|
|
225
|
+
confirmButtonColor: data.type == 'delete' ? '#D12114' : '#0072B9',
|
|
226
|
+
customClass: {
|
|
227
|
+
cancelButton: 'custom-cancel-button',
|
|
228
|
+
},
|
|
229
|
+
}).then((result) => result.isConfirmed);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Injectable, signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root'
|
|
5
|
+
})
|
|
6
|
+
export class ListenerService {
|
|
7
|
+
|
|
8
|
+
$loading: any = signal<{ load: boolean; message?: any }>({
|
|
9
|
+
load: false,
|
|
10
|
+
message: null,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
constructor() { }
|
|
14
|
+
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { InputTextComponent } from '@shared/ui/input-text/input-text.component';
|
|
5
|
+
|
|
6
|
+
@NgModule({
|
|
7
|
+
imports: [
|
|
8
|
+
ReactiveFormsModule,
|
|
9
|
+
FormsModule,
|
|
10
|
+
InputTextComponent
|
|
11
|
+
],
|
|
12
|
+
exports: [ReactiveFormsModule, FormsModule, InputTextComponent],
|
|
13
|
+
})
|
|
14
|
+
export class LibraryModule { }
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Component, forwardRef, input, Input, OnInit } from '@angular/core';
|
|
2
|
+
import { ControlValueAccessor, FormControl, FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms';
|
|
3
|
+
import { AlertErrorInputComponent } from '@shared/widgets/alert-error-input/alert-error-input.component';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'input-text',
|
|
7
|
+
styleUrls: ['./input-text.component.scss'],
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [ReactiveFormsModule, FormsModule, AlertErrorInputComponent],
|
|
10
|
+
providers: [
|
|
11
|
+
{
|
|
12
|
+
provide: NG_VALUE_ACCESSOR,
|
|
13
|
+
useExisting: forwardRef(() => InputTextComponent),
|
|
14
|
+
multi: true,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
template: `
|
|
18
|
+
<input (ngModelChange)="onValueChange($event)" [(ngModel)]="value" type="email"
|
|
19
|
+
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
20
|
+
[placeholder]="placeholder()" />
|
|
21
|
+
<alert-error [control]="control" [label]="label()"></alert-error>
|
|
22
|
+
`
|
|
23
|
+
})
|
|
24
|
+
export class InputTextComponent implements ControlValueAccessor, OnInit {
|
|
25
|
+
|
|
26
|
+
@Input() control!: FormControl | any;
|
|
27
|
+
placeholder = input('Enter your email'); //default
|
|
28
|
+
label = input('label'); //default
|
|
29
|
+
|
|
30
|
+
value: any;
|
|
31
|
+
|
|
32
|
+
constructor() { }
|
|
33
|
+
|
|
34
|
+
ngOnInit() {
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
writeValue(value: any): void {
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
registerOnChange(fn: any): void {
|
|
41
|
+
this.onChange = fn;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
registerOnTouched(fn: any): void {
|
|
45
|
+
this.onTouched = fn;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
hasError(error: string): boolean | null {
|
|
49
|
+
// console.log(this.control.errors);
|
|
50
|
+
return (
|
|
51
|
+
this.control?.hasError(error) &&
|
|
52
|
+
(this.control?.touched || this.control?.dirty)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onValueChange(value: any) {
|
|
57
|
+
this.onChange(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fungsi yang akan dipanggil ketika nilai diubah
|
|
61
|
+
onChange: (date: any) => void = () => { };
|
|
62
|
+
onTouched: () => void = () => { };
|
|
63
|
+
|
|
64
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
|
|
2
|
+
|
|
3
|
+
function emailPatternValidator(): ValidatorFn {
|
|
4
|
+
return (control: AbstractControl): ValidationErrors | null => {
|
|
5
|
+
const value = control.value;
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
|
|
8
|
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
9
|
+
return emailRegex.test(value) ? null : { invalidEmailPattern: true };
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function linkPatternValidator(): ValidatorFn {
|
|
14
|
+
return (control: AbstractControl): ValidationErrors | null => {
|
|
15
|
+
const value = control.value;
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
|
|
18
|
+
const regex =
|
|
19
|
+
/^https:\/\/([\da-z.-]+)\.([a-z.]{2,6})([\/\w .-]*)*\/?(\?.*)?$/;
|
|
20
|
+
return regex.test(value) ? null : { invalidLinkPattern: true };
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { emailPatternValidator, linkPatternValidator };
|
|
25
|
+
|
package/bin/utils/resource/src/app/shared/widgets/alert-error-input/alert-error-input.component.html
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
@if (hasError('required')) {
|
|
2
|
+
<small class="text-[#EB5757]">
|
|
3
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
4
|
+
{{label}} diperlukan
|
|
5
|
+
</div>
|
|
6
|
+
</small>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@if (hasError('invalidLinkPattern')) {
|
|
10
|
+
<small class="text-[#eb5757]">
|
|
11
|
+
Format {{label}} salah!, Contoh: https://gmeet.google.com/
|
|
12
|
+
</small>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@if (hasError('invalidEmailPattern')) {
|
|
16
|
+
<small class="text-[#eb5757]">
|
|
17
|
+
Format {{label}} salah!
|
|
18
|
+
</small>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@if (hasError('dateRange')) {
|
|
23
|
+
<small class="text-[#eb5757]">
|
|
24
|
+
{{control?.errors['dateRange']}}
|
|
25
|
+
</small>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@if (hasError('totalPriceExceed')) {
|
|
29
|
+
<small class="text-[#eb5757]">
|
|
30
|
+
Total tidak boleh lebih besar dari Total Negosiasi
|
|
31
|
+
</small>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@if (hasError('minlength')) {
|
|
35
|
+
<small class="text-[#eb5757]">
|
|
36
|
+
{{label}} Minimal {{ control?.errors?.minlength?.requiredLength }}
|
|
37
|
+
Error
|
|
38
|
+
</small>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@if (hasError('maxlength')) {
|
|
42
|
+
<small class="text-[#eb5757]">
|
|
43
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
44
|
+
{{label}} Maksimal {{ control?.errors?.maxlength?.requiredLength }}
|
|
45
|
+
Karakter
|
|
46
|
+
</div>
|
|
47
|
+
</small>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@if (hasError('min')) {
|
|
51
|
+
<small class="text-[#eb5757]">
|
|
52
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
53
|
+
{{label}} Minimal {{ control?.errors?.min?.min }}
|
|
54
|
+
</div>
|
|
55
|
+
</small>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@if (hasError('max')) {
|
|
59
|
+
<small class="text-[#eb5757]">
|
|
60
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
61
|
+
{{label}} Maksimal {{ control?.errors?.max?.max }}
|
|
62
|
+
</div>
|
|
63
|
+
</small>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@if (hasError('mismatch')) {
|
|
67
|
+
<small class="text-[#eb5757]">
|
|
68
|
+
tidak sama
|
|
69
|
+
</small>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@if (hasError('passwordSameAsCurrent')) {
|
|
73
|
+
<small class="text-[#eb5757]">
|
|
74
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
75
|
+
Password baru tidak boleh sama dengan password lama
|
|
76
|
+
</div>
|
|
77
|
+
</small>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@if (hasError('passwordMismatch')) {
|
|
81
|
+
<small class="text-[#eb5757]">
|
|
82
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
83
|
+
Password baru dan konfirmasi password baru tidak sama
|
|
84
|
+
</div>
|
|
85
|
+
</small>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@if (hasError('strongPassword')) {
|
|
89
|
+
<small class="text-[#eb5757]">
|
|
90
|
+
<div class="w-full my-2 bg-red-100 rounded-[4px] px-[12px] py-[4px]">
|
|
91
|
+
Password harus mengandung minimal 8 karakter, huruf besar, huruf kecil, angka, dan karakter spesial.
|
|
92
|
+
</div>
|
|
93
|
+
</small>
|
|
94
|
+
}
|
package/bin/utils/resource/src/app/shared/widgets/alert-error-input/alert-error-input.component.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Component, Input, OnInit } from '@angular/core';
|
|
2
|
+
import { FormControl } from '@angular/forms';
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: 'alert-error',
|
|
6
|
+
templateUrl: './alert-error-input.component.html',
|
|
7
|
+
standalone: true,
|
|
8
|
+
})
|
|
9
|
+
export class AlertErrorInputComponent implements OnInit {
|
|
10
|
+
@Input() control!: FormControl | any;
|
|
11
|
+
@Input() label!: string;
|
|
12
|
+
@Input() secondLabel!: string;
|
|
13
|
+
constructor() { }
|
|
14
|
+
|
|
15
|
+
ngOnInit() { }
|
|
16
|
+
|
|
17
|
+
hasError(error: string): boolean {
|
|
18
|
+
//console.log(error, this.control.errors);
|
|
19
|
+
return (
|
|
20
|
+
this.control?.hasError(error) &&
|
|
21
|
+
(this.control?.touched || this.control?.dirty)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<div class="fixed inset-0 bgs z-50 flex items-center justify-center">
|
|
2
|
+
<div class="absolute transition-all duration-100 ease-in-out transform hover:scale-110"
|
|
3
|
+
style="padding: 50px; box-sizing: content-box;"
|
|
4
|
+
[ngStyle]="{ top: boxPosition.top, left: boxPosition.left, position: 'absolute' }" (mouseenter)="onMouseEnterBox()">
|
|
5
|
+
|
|
6
|
+
<div (click)="onLogout()"
|
|
7
|
+
class=" w-40 h-20 bg-black text-white flex items-center text-center justify-center cursor-pointer rounded-xl shadow-lg">
|
|
8
|
+
Tangkap
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="absolute top-5 right-5 text-white text-xl space-y-2 text-right bg-amber-900 p-2 rounded-2xl">
|
|
13
|
+
<div>Waktu: {{ timer }} detik</div>
|
|
14
|
+
<div>Skor: {{ score }}</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div *ngIf="gameEnded" class="absolute bottom-10 text-white text-2xl font-bold">
|
|
18
|
+
Permainan Selesai! Skor akhir: {{ score }}
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
selector: 'app-widget_game',
|
|
5
|
+
templateUrl: './widget_game.component.html',
|
|
6
|
+
styleUrls: ['./widget_game.component.scss'],
|
|
7
|
+
standalone: false
|
|
8
|
+
})
|
|
9
|
+
export class Widget_gameComponent implements OnInit {
|
|
10
|
+
|
|
11
|
+
boxPosition = { top: '50%', left: '50%' };
|
|
12
|
+
gameStarted = false;
|
|
13
|
+
gameEnded = false;
|
|
14
|
+
timer = 60;
|
|
15
|
+
score = 0;
|
|
16
|
+
intervalId: any;
|
|
17
|
+
constructor(
|
|
18
|
+
private cdr: ChangeDetectorRef
|
|
19
|
+
) { }
|
|
20
|
+
|
|
21
|
+
ngOnInit() {
|
|
22
|
+
this.startGame();
|
|
23
|
+
}
|
|
24
|
+
startGame() {
|
|
25
|
+
this.gameStarted = true;
|
|
26
|
+
this.timer = 60;
|
|
27
|
+
this.score = 0;
|
|
28
|
+
this.gameEnded = false;
|
|
29
|
+
this.moveBoxRandomly();
|
|
30
|
+
|
|
31
|
+
this.intervalId = setInterval(() => {
|
|
32
|
+
this.timer--;
|
|
33
|
+
if (this.timer <= 0) {
|
|
34
|
+
clearInterval(this.intervalId);
|
|
35
|
+
this.gameEnded = true;
|
|
36
|
+
}
|
|
37
|
+
this.cdr.detectChanges();
|
|
38
|
+
}, 1000);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
onMouseEnterBox() {
|
|
42
|
+
if (!this.gameEnded) {
|
|
43
|
+
this.score++;
|
|
44
|
+
this.moveBoxRandomly();
|
|
45
|
+
// this.cdr.detectChanges();
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
moveBoxRandomly() {
|
|
51
|
+
const top = Math.random() * 90;
|
|
52
|
+
const left = Math.random() * 90;
|
|
53
|
+
this.boxPosition = {
|
|
54
|
+
top: `${top}%`,
|
|
55
|
+
left: `${left}%`,
|
|
56
|
+
};
|
|
57
|
+
this.cdr.detectChanges();
|
|
58
|
+
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onLogout() {
|
|
62
|
+
// this.auth.onLogout();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { Widget_gameComponent } from './widget_game.component';
|
|
4
|
+
|
|
5
|
+
@NgModule({
|
|
6
|
+
imports: [
|
|
7
|
+
CommonModule
|
|
8
|
+
],
|
|
9
|
+
exports: [Widget_gameComponent],
|
|
10
|
+
declarations: [Widget_gameComponent]
|
|
11
|
+
})
|
|
12
|
+
export class Widget_gameModule { }
|
package/package.json
CHANGED