unictive-angular 1.0.28 → 1.0.30
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/package.json +1 -0
- package/bin/utils/resource/src/app/API/README.md +10 -0
- package/bin/utils/resource/src/app/API/user.service.ts +26 -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/app.ts +18 -2
- 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/interceptors/jwt.interceptor.ts +1 -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/models/entities/user.entity.ts +15 -0
- package/bin/utils/resource/src/app/core/respositories/screen_repo/screen.repo.ts +19 -0
- package/bin/utils/resource/src/app/core/respository/screen_repo/screen.repo.ts +27 -0
- package/bin/utils/resource/src/app/core/respository/user/user_repo.ts +26 -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,51 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { logger } from '@shared/utils/logger';
|
|
3
|
+
|
|
4
|
+
export interface SingleState<T> {
|
|
5
|
+
value: T | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function singleStoreFactory<T>() {
|
|
11
|
+
const _state = signal<SingleState<T>>({ value: null, loading: false });
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
state: _state,
|
|
15
|
+
|
|
16
|
+
// Mutations
|
|
17
|
+
set(value: T) {
|
|
18
|
+
_state.set({ value, loading: false });
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
reset() {
|
|
22
|
+
_state.set({ value: null, loading: false });
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
patch(partial: Partial<T>) {
|
|
26
|
+
_state.update((curr) => ({
|
|
27
|
+
...curr,
|
|
28
|
+
value: curr.value ? { ...curr.value, ...partial } : null
|
|
29
|
+
}));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
clear() {
|
|
33
|
+
_state.set({ value: null, loading: false });
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Async Loader
|
|
37
|
+
async load(fetchFn: () => Promise<T>) {
|
|
38
|
+
_state.update((curr) => ({ ...curr, loading: true, error: undefined }));
|
|
39
|
+
try {
|
|
40
|
+
const data = await fetchFn();
|
|
41
|
+
_state.set({ value: data, loading: false });
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
_state.update((curr) => ({
|
|
44
|
+
...curr,
|
|
45
|
+
loading: false,
|
|
46
|
+
error: err.message ?? 'Unknown error'
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -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,15 @@
|
|
|
1
|
+
// import { entityStoreFactory } from "@core/config/entity-store";
|
|
2
|
+
import { singleStoreFactory } from "@core/config/single-store";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export interface User_m {
|
|
7
|
+
username: string;
|
|
8
|
+
email: string;
|
|
9
|
+
id: number;
|
|
10
|
+
role: string;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export const Entity_User = singleStoreFactory<User_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,27 @@
|
|
|
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
|
+
import { Entity_User } from '@core/models/entities/user.entity';
|
|
6
|
+
import { UserService } from '@api/user.service';
|
|
7
|
+
|
|
8
|
+
@Injectable({ providedIn: 'root' })
|
|
9
|
+
export class RepoScreen {
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private api: UserService
|
|
14
|
+
) { }
|
|
15
|
+
|
|
16
|
+
load_user() {
|
|
17
|
+
Entity_User.load(async () => {
|
|
18
|
+
const res = await this.api.get_user();
|
|
19
|
+
const response = res?.data ?? [];
|
|
20
|
+
return response; // hanya return user
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get getData() {
|
|
25
|
+
return ScreenEntity.state();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Injectable } from "@angular/core";
|
|
2
|
+
import { UserService } from "@api/user.service";
|
|
3
|
+
import { Entity_User } from "@core/models/entities/user.entity";
|
|
4
|
+
|
|
5
|
+
@Injectable({ providedIn: 'root' })
|
|
6
|
+
export class Repo_User {
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
private api: UserService
|
|
11
|
+
) { }
|
|
12
|
+
|
|
13
|
+
load_user() {
|
|
14
|
+
Entity_User.load(async () => {
|
|
15
|
+
const res = await this.api.get_user();
|
|
16
|
+
const response = res?.data ?? null;
|
|
17
|
+
return response; // hanya return user
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
get getData() {
|
|
23
|
+
return Entity_User.state();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|