create-nativecore 0.1.1 → 0.2.0
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/README.md +6 -14
- package/bin/index.mjs +402 -431
- package/package.json +3 -2
- package/template/.env.example +28 -0
- package/template/.htmlhintrc +14 -0
- package/template/api/data/dashboard.json +11 -0
- package/template/api/data/users.json +18 -0
- package/template/api/mockApi.js +161 -0
- package/template/assets/icon.svg +13 -0
- package/template/assets/logo.svg +25 -0
- package/template/eslint.config.js +94 -0
- package/template/index.html +137 -0
- package/template/manifest.json +19 -0
- package/template/public/.well-known/security.txt +9 -0
- package/template/public/_headers +24 -0
- package/template/public/_redirects +14 -0
- package/template/public/assets/icon.svg +13 -0
- package/template/public/assets/logo.svg +25 -0
- package/template/public/manifest.json +19 -0
- package/template/public/robots.txt +13 -0
- package/template/public/sitemap.xml +27 -0
- package/template/scripts/build-for-bots.mjs +121 -0
- package/template/scripts/convert-to-ts.mjs +106 -0
- package/template/scripts/fix-encoding.mjs +38 -0
- package/template/scripts/fix-svg-paths.mjs +32 -0
- package/template/scripts/generate-cf-router.mjs +52 -0
- package/template/scripts/inject-dev-tools.mjs +41 -0
- package/template/scripts/inject-version.mjs +65 -0
- package/template/scripts/make-component.mjs +445 -0
- package/template/scripts/make-component.mjs.backup +432 -0
- package/template/scripts/make-controller.mjs +119 -0
- package/template/scripts/make-core-component.mjs +303 -0
- package/template/scripts/make-view.mjs +346 -0
- package/template/scripts/minify.mjs +71 -0
- package/template/scripts/prepare-static-assets.mjs +141 -0
- package/template/scripts/prompt-bot-build.mjs +223 -0
- package/template/scripts/remove-component.mjs +170 -0
- package/template/scripts/remove-core-component.mjs +156 -0
- package/template/scripts/remove-dev.mjs +13 -0
- package/template/scripts/remove-view.mjs +200 -0
- package/template/scripts/strip-dev-blocks.mjs +30 -0
- package/template/scripts/watch-compile.mjs +69 -0
- package/template/server.js +1066 -0
- package/template/src/app.ts +115 -0
- package/template/src/components/appRegistry.ts +8 -0
- package/template/src/components/core/app-footer.ts +27 -0
- package/template/src/components/core/app-header.ts +175 -0
- package/template/src/components/core/app-sidebar.ts +238 -0
- package/template/src/components/core/loading-spinner.ts +25 -0
- package/template/src/components/core/nc-a.ts +313 -0
- package/template/src/components/core/nc-accordion.ts +186 -0
- package/template/src/components/core/nc-alert.ts +153 -0
- package/template/src/components/core/nc-animation.ts +1150 -0
- package/template/src/components/core/nc-autocomplete.ts +271 -0
- package/template/src/components/core/nc-avatar-group.ts +113 -0
- package/template/src/components/core/nc-avatar.ts +148 -0
- package/template/src/components/core/nc-badge.ts +86 -0
- package/template/src/components/core/nc-bottom-nav.ts +214 -0
- package/template/src/components/core/nc-breadcrumb.ts +96 -0
- package/template/src/components/core/nc-button.ts +307 -0
- package/template/src/components/core/nc-card.ts +160 -0
- package/template/src/components/core/nc-checkbox.ts +282 -0
- package/template/src/components/core/nc-chip.ts +115 -0
- package/template/src/components/core/nc-code.ts +314 -0
- package/template/src/components/core/nc-collapsible.ts +154 -0
- package/template/src/components/core/nc-color-picker.ts +268 -0
- package/template/src/components/core/nc-copy-button.ts +119 -0
- package/template/src/components/core/nc-date-picker.ts +443 -0
- package/template/src/components/core/nc-div.ts +280 -0
- package/template/src/components/core/nc-divider.ts +81 -0
- package/template/src/components/core/nc-drawer.ts +230 -0
- package/template/src/components/core/nc-dropdown.ts +178 -0
- package/template/src/components/core/nc-empty-state.ts +134 -0
- package/template/src/components/core/nc-file-upload.ts +354 -0
- package/template/src/components/core/nc-form.ts +312 -0
- package/template/src/components/core/nc-image.ts +184 -0
- package/template/src/components/core/nc-input.ts +383 -0
- package/template/src/components/core/nc-kbd.ts +48 -0
- package/template/src/components/core/nc-menu-item.ts +193 -0
- package/template/src/components/core/nc-menu.ts +376 -0
- package/template/src/components/core/nc-modal.ts +238 -0
- package/template/src/components/core/nc-nav-item.ts +151 -0
- package/template/src/components/core/nc-number-input.ts +350 -0
- package/template/src/components/core/nc-otp-input.ts +235 -0
- package/template/src/components/core/nc-pagination.ts +178 -0
- package/template/src/components/core/nc-popover.ts +260 -0
- package/template/src/components/core/nc-progress-circular.ts +119 -0
- package/template/src/components/core/nc-progress.ts +134 -0
- package/template/src/components/core/nc-radio.ts +235 -0
- package/template/src/components/core/nc-rating.ts +266 -0
- package/template/src/components/core/nc-rich-text.ts +283 -0
- package/template/src/components/core/nc-scroll-top.ts +116 -0
- package/template/src/components/core/nc-select.ts +452 -0
- package/template/src/components/core/nc-skeleton.ts +107 -0
- package/template/src/components/core/nc-slider.ts +285 -0
- package/template/src/components/core/nc-snackbar.ts +230 -0
- package/template/src/components/core/nc-splash.ts +343 -0
- package/template/src/components/core/nc-stepper.ts +247 -0
- package/template/src/components/core/nc-switch.ts +281 -0
- package/template/src/components/core/nc-tab-item.ts +138 -0
- package/template/src/components/core/nc-table.ts +279 -0
- package/template/src/components/core/nc-tabs.ts +554 -0
- package/template/src/components/core/nc-tag-input.ts +279 -0
- package/template/src/components/core/nc-textarea.ts +216 -0
- package/template/src/components/core/nc-time-picker.ts +438 -0
- package/template/src/components/core/nc-timeline.ts +186 -0
- package/template/src/components/core/nc-tooltip.ts +143 -0
- package/template/src/components/frameworkRegistry.ts +68 -0
- package/template/src/components/preloadRegistry.ts +28 -0
- package/template/src/components/registry.ts +8 -0
- package/template/src/components/ui/dashboard-signal-lab.ts +284 -0
- package/template/src/constants/apiEndpoints.ts +27 -0
- package/template/src/constants/errorMessages.ts +23 -0
- package/template/src/constants/index.ts +8 -0
- package/template/src/constants/routePaths.ts +15 -0
- package/template/src/constants/storageKeys.ts +18 -0
- package/template/src/controllers/dashboard.controller.ts +200 -0
- package/template/src/controllers/home.controller.ts +21 -0
- package/template/src/controllers/index.ts +11 -0
- package/template/src/controllers/login.controller.ts +131 -0
- package/template/src/core/component.ts +354 -0
- package/template/src/core/errorHandler.ts +85 -0
- package/template/src/core/gpu-animation.ts +604 -0
- package/template/src/core/http.ts +173 -0
- package/template/src/core/lazyComponents.ts +90 -0
- package/template/src/core/router.ts +642 -0
- package/template/src/core/signals.ts +146 -0
- package/template/src/core/state.ts +248 -0
- package/template/src/dev/component-editor.ts +1363 -0
- package/template/src/dev/component-overlay.ts +278 -0
- package/template/src/dev/context-menu.ts +223 -0
- package/template/src/dev/denc-tools.ts +250 -0
- package/template/src/dev/hmr.ts +189 -0
- package/template/src/dev/nfbs.code-workspace +27 -0
- package/template/src/dev/outline-panel.ts +1247 -0
- package/template/src/middleware/auth.middleware.ts +23 -0
- package/template/src/routes/routes.ts +38 -0
- package/template/src/services/api.service.ts +394 -0
- package/template/src/services/auth.service.ts +176 -0
- package/template/src/services/index.ts +8 -0
- package/template/src/services/logger.service.ts +74 -0
- package/template/src/services/storage.service.ts +88 -0
- package/template/src/stores/appStore.ts +57 -0
- package/template/src/stores/uiStore.ts +36 -0
- package/template/src/styles/core-variables.css +219 -0
- package/template/src/styles/core.css +710 -0
- package/template/src/styles/main.css +3164 -0
- package/template/src/styles/variables.css +152 -0
- package/template/src/types/global.d.ts +47 -0
- package/template/src/utils/cacheBuster.ts +20 -0
- package/template/src/utils/dom.ts +149 -0
- package/template/src/utils/events.ts +203 -0
- package/template/src/utils/form.ts +176 -0
- package/template/src/utils/formatters.ts +169 -0
- package/template/src/utils/helpers.ts +195 -0
- package/template/src/utils/markdown.ts +307 -0
- package/template/src/utils/sidebar.ts +96 -0
- package/template/src/utils/smoothScroll.ts +85 -0
- package/template/src/utils/templates.ts +23 -0
- package/template/src/utils/validation.ts +73 -0
- package/template/src/views/protected/dashboard.html +293 -0
- package/template/src/views/public/home.html +150 -0
- package/template/src/views/public/login.html +102 -0
- package/template/tests/unit/component.test.ts +87 -0
- package/template/tests/unit/computed.test.ts +79 -0
- package/template/tests/unit/form.test.ts +68 -0
- package/template/tests/unit/formatters.test.ts +49 -0
- package/template/tests/unit/lazy-components.test.ts +59 -0
- package/template/tests/unit/markdown.test.ts +62 -0
- package/template/tests/unit/router.test.ts +112 -0
- package/template/tests/unit/signals.test.ts +54 -0
- package/template/tests/unit/validation.test.ts +50 -0
- package/template/tsconfig.build.json +21 -0
- package/template/tsconfig.json +51 -0
- package/template/vitest.config.ts +36 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { computed, useState } from '@core/state.js';
|
|
2
|
+
import type { ComputedState, State } from '@core/state.js';
|
|
3
|
+
import { validateForm } from '@utils/validation.js';
|
|
4
|
+
|
|
5
|
+
type Validator = (value: any) => boolean;
|
|
6
|
+
type FieldStates<T extends Record<string, any>> = { [K in keyof T]: State<T[K]> };
|
|
7
|
+
type FieldFlags<T extends Record<string, any>> = { [K in keyof T]: State<boolean> };
|
|
8
|
+
type ValidationRules<T extends Record<string, any>> = Partial<Record<keyof T, Validator[]>>;
|
|
9
|
+
|
|
10
|
+
export interface UseFormOptions<T extends Record<string, any>> {
|
|
11
|
+
initialValues: T;
|
|
12
|
+
rules?: ValidationRules<T>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseFormResult<T extends Record<string, any>> {
|
|
16
|
+
fields: FieldStates<T>;
|
|
17
|
+
touched: FieldFlags<T>;
|
|
18
|
+
dirty: FieldFlags<T>;
|
|
19
|
+
errors: ComputedState<Record<string, string>>;
|
|
20
|
+
isDirty: ComputedState<boolean>;
|
|
21
|
+
isValid: ComputedState<boolean>;
|
|
22
|
+
getValues(): T;
|
|
23
|
+
reset(values?: Partial<T>): void;
|
|
24
|
+
markAsPristine(): void;
|
|
25
|
+
bindField<K extends keyof T>(fieldName: K, target: string | HTMLElement): () => void;
|
|
26
|
+
submit(handler: (values: T) => void | Promise<void>): (event?: Event) => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useForm<T extends Record<string, any>>(options: UseFormOptions<T>): UseFormResult<T> {
|
|
30
|
+
const initialSnapshot = { ...options.initialValues };
|
|
31
|
+
const fields = {} as FieldStates<T>;
|
|
32
|
+
const touched = {} as FieldFlags<T>;
|
|
33
|
+
const dirty = {} as FieldFlags<T>;
|
|
34
|
+
|
|
35
|
+
for (const [key, value] of Object.entries(options.initialValues)) {
|
|
36
|
+
const typedKey = key as keyof T;
|
|
37
|
+
fields[typedKey] = useState(value as T[keyof T]);
|
|
38
|
+
touched[typedKey] = useState(false);
|
|
39
|
+
dirty[typedKey] = useState(false);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const errors = computed(() => validateForm(getValues(), options.rules ?? {}));
|
|
43
|
+
const isDirty = computed(() => Object.values(dirty).some(flag => flag.value));
|
|
44
|
+
const isValid = computed(() => Object.keys(errors.value).length === 0);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
fields,
|
|
48
|
+
touched,
|
|
49
|
+
dirty,
|
|
50
|
+
errors,
|
|
51
|
+
isDirty,
|
|
52
|
+
isValid,
|
|
53
|
+
getValues,
|
|
54
|
+
reset,
|
|
55
|
+
markAsPristine,
|
|
56
|
+
bindField,
|
|
57
|
+
submit
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function getValues(): T {
|
|
61
|
+
const values = {} as T;
|
|
62
|
+
|
|
63
|
+
for (const key of Object.keys(fields) as Array<keyof T>) {
|
|
64
|
+
values[key] = fields[key].value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return values;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reset(values: Partial<T> = {}): void {
|
|
71
|
+
for (const key of Object.keys(fields) as Array<keyof T>) {
|
|
72
|
+
const nextValue = key in values ? values[key] : initialSnapshot[key];
|
|
73
|
+
fields[key].value = nextValue as T[keyof T];
|
|
74
|
+
touched[key].value = false;
|
|
75
|
+
dirty[key].value = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function markAsPristine(): void {
|
|
80
|
+
for (const key of Object.keys(fields) as Array<keyof T>) {
|
|
81
|
+
touched[key].value = false;
|
|
82
|
+
dirty[key].value = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function bindField<K extends keyof T>(fieldName: K, target: string | HTMLElement): () => void {
|
|
87
|
+
const element = resolveFieldElement(target);
|
|
88
|
+
if (!element) return () => {};
|
|
89
|
+
|
|
90
|
+
syncElementFromState(fieldName, element);
|
|
91
|
+
|
|
92
|
+
const inputHandler = () => {
|
|
93
|
+
fields[fieldName].value = readElementValue(element) as T[K];
|
|
94
|
+
touched[fieldName].value = true;
|
|
95
|
+
dirty[fieldName].value = !Object.is(fields[fieldName].value, initialSnapshot[fieldName]);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const blurHandler = () => {
|
|
99
|
+
touched[fieldName].value = true;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const eventName = getBindingEventName(element);
|
|
103
|
+
element.addEventListener(eventName, inputHandler);
|
|
104
|
+
element.addEventListener('blur', blurHandler);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
element.removeEventListener(eventName, inputHandler);
|
|
108
|
+
element.removeEventListener('blur', blurHandler);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function submit(handler: (values: T) => void | Promise<void>): (event?: Event) => Promise<boolean> {
|
|
113
|
+
return async (event?: Event) => {
|
|
114
|
+
event?.preventDefault();
|
|
115
|
+
|
|
116
|
+
for (const key of Object.keys(touched) as Array<keyof T>) {
|
|
117
|
+
touched[key].value = true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isValid.value) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await handler(getValues());
|
|
125
|
+
markAsPristine();
|
|
126
|
+
return true;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function syncElementFromState<K extends keyof T>(fieldName: K, element: FormControlElement): void {
|
|
131
|
+
const value = fields[fieldName].value;
|
|
132
|
+
|
|
133
|
+
if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
|
|
134
|
+
element.checked = Boolean(value);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
element.value = value == null ? '' : String(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type FormControlElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
143
|
+
|
|
144
|
+
function resolveFieldElement(target: string | HTMLElement): FormControlElement | null {
|
|
145
|
+
const element = typeof target === 'string' ? document.querySelector<HTMLElement>(target) : target;
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
element instanceof HTMLInputElement ||
|
|
149
|
+
element instanceof HTMLTextAreaElement ||
|
|
150
|
+
element instanceof HTMLSelectElement
|
|
151
|
+
) {
|
|
152
|
+
return element;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getBindingEventName(element: FormControlElement): 'input' | 'change' {
|
|
159
|
+
if (element instanceof HTMLSelectElement) {
|
|
160
|
+
return 'change';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
|
|
164
|
+
return 'change';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return 'input';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readElementValue(element: FormControlElement): string | boolean {
|
|
171
|
+
if (element instanceof HTMLInputElement && (element.type === 'checkbox' || element.type === 'radio')) {
|
|
172
|
+
return element.checked;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return element.value;
|
|
176
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatter Utilities
|
|
3
|
+
* Common formatting functions for display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format currency
|
|
8
|
+
* @param {number} amount - Amount to format
|
|
9
|
+
* @param {string} currency - Currency code (default: USD)
|
|
10
|
+
* @returns {string} Formatted currency
|
|
11
|
+
*/
|
|
12
|
+
export function formatCurrency(amount, currency = 'USD') {
|
|
13
|
+
return new Intl.NumberFormat('en-US', {
|
|
14
|
+
style: 'currency',
|
|
15
|
+
currency,
|
|
16
|
+
}).format(amount);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format date
|
|
21
|
+
* @param {Date|string} date - Date to format
|
|
22
|
+
* @param {string} format - Format style (short, medium, long, full)
|
|
23
|
+
* @returns {string} Formatted date
|
|
24
|
+
*/
|
|
25
|
+
export function formatDate(date, format = 'medium') {
|
|
26
|
+
const d = new Date(date);
|
|
27
|
+
const options = {
|
|
28
|
+
short: { month: 'numeric', day: 'numeric', year: '2-digit' },
|
|
29
|
+
medium: { month: 'short', day: 'numeric', year: 'numeric' },
|
|
30
|
+
long: { month: 'long', day: 'numeric', year: 'numeric' },
|
|
31
|
+
full: { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return new Intl.DateTimeFormat('en-US', options[format] || options.medium).format(d);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Format time
|
|
39
|
+
* @param {Date|string} date - Date to format
|
|
40
|
+
* @returns {string} Formatted time
|
|
41
|
+
*/
|
|
42
|
+
export function formatTime(date) {
|
|
43
|
+
const d = new Date(date);
|
|
44
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
45
|
+
hour: 'numeric',
|
|
46
|
+
minute: '2-digit',
|
|
47
|
+
hour12: true,
|
|
48
|
+
}).format(d);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Format date and time
|
|
53
|
+
* @param {Date|string} date - Date to format
|
|
54
|
+
* @returns {string} Formatted date and time
|
|
55
|
+
*/
|
|
56
|
+
export function formatDateTime(date) {
|
|
57
|
+
return `${formatDate(date)} at ${formatTime(date)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Format relative time (e.g., "2 hours ago")
|
|
62
|
+
* @param {Date|string} date - Date to format
|
|
63
|
+
* @returns {string} Relative time string
|
|
64
|
+
*/
|
|
65
|
+
export function formatRelativeTime(date: Date | string): string {
|
|
66
|
+
const d = new Date(date);
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const seconds = Math.floor((now.getTime() - d.getTime()) / 1000);
|
|
69
|
+
|
|
70
|
+
const intervals = {
|
|
71
|
+
year: 31536000,
|
|
72
|
+
month: 2592000,
|
|
73
|
+
week: 604800,
|
|
74
|
+
day: 86400,
|
|
75
|
+
hour: 3600,
|
|
76
|
+
minute: 60,
|
|
77
|
+
second: 1,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
81
|
+
const interval = Math.floor(seconds / secondsInUnit);
|
|
82
|
+
if (interval >= 1) {
|
|
83
|
+
return interval === 1 ? `1 ${unit} ago` : `${interval} ${unit}s ago`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return 'just now';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format number with thousands separator
|
|
92
|
+
* @param {number} num - Number to format
|
|
93
|
+
* @returns {string} Formatted number
|
|
94
|
+
*/
|
|
95
|
+
export function formatNumber(num) {
|
|
96
|
+
return new Intl.NumberFormat('en-US').format(num);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Format file size
|
|
101
|
+
* @param {number} bytes - Size in bytes
|
|
102
|
+
* @returns {string} Formatted size
|
|
103
|
+
*/
|
|
104
|
+
export function formatFileSize(bytes) {
|
|
105
|
+
if (bytes === 0) return '0 Bytes';
|
|
106
|
+
|
|
107
|
+
const k = 1024;
|
|
108
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
109
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
110
|
+
|
|
111
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Format percentage
|
|
116
|
+
* @param {number} value - Value to format (0-1 or 0-100)
|
|
117
|
+
* @param {number} decimals - Decimal places
|
|
118
|
+
* @returns {string} Formatted percentage
|
|
119
|
+
*/
|
|
120
|
+
export function formatPercentage(value, decimals = 0) {
|
|
121
|
+
const percent = value <= 1 ? value * 100 : value;
|
|
122
|
+
return `${percent.toFixed(decimals)}%`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Truncate text
|
|
127
|
+
* @param {string} text - Text to truncate
|
|
128
|
+
* @param {number} maxLength - Maximum length
|
|
129
|
+
* @param {string} suffix - Suffix to add (default: '...')
|
|
130
|
+
* @returns {string} Truncated text
|
|
131
|
+
*/
|
|
132
|
+
export function truncate(text, maxLength, suffix = '...') {
|
|
133
|
+
if (!text || text.length <= maxLength) return text;
|
|
134
|
+
return text.substring(0, maxLength - suffix.length) + suffix;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Capitalize first letter
|
|
139
|
+
* @param {string} str - String to capitalize
|
|
140
|
+
* @returns {string} Capitalized string
|
|
141
|
+
*/
|
|
142
|
+
export function capitalize(str) {
|
|
143
|
+
if (!str) return '';
|
|
144
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Title case
|
|
149
|
+
* @param {string} str - String to convert
|
|
150
|
+
* @returns {string} Title cased string
|
|
151
|
+
*/
|
|
152
|
+
export function titleCase(str) {
|
|
153
|
+
if (!str) return '';
|
|
154
|
+
return str.toLowerCase().split(' ').map(capitalize).join(' ');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default {
|
|
158
|
+
formatCurrency,
|
|
159
|
+
formatDate,
|
|
160
|
+
formatTime,
|
|
161
|
+
formatDateTime,
|
|
162
|
+
formatRelativeTime,
|
|
163
|
+
formatNumber,
|
|
164
|
+
formatFileSize,
|
|
165
|
+
formatPercentage,
|
|
166
|
+
truncate,
|
|
167
|
+
capitalize,
|
|
168
|
+
titleCase,
|
|
169
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility Helper Functions
|
|
3
|
+
* Reusable utility functions for common tasks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Debounce function calls
|
|
8
|
+
* @param {Function} func - Function to debounce
|
|
9
|
+
* @param {number} wait - Wait time in milliseconds
|
|
10
|
+
*/
|
|
11
|
+
export function debounce(func, wait = 300) {
|
|
12
|
+
let timeout;
|
|
13
|
+
return function executedFunction(...args) {
|
|
14
|
+
const later = () => {
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
func(...args);
|
|
17
|
+
};
|
|
18
|
+
clearTimeout(timeout);
|
|
19
|
+
timeout = setTimeout(later, wait);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Throttle function calls
|
|
25
|
+
* @param {Function} func - Function to throttle
|
|
26
|
+
* @param {number} limit - Time limit in milliseconds
|
|
27
|
+
*/
|
|
28
|
+
export function throttle(func, limit = 300) {
|
|
29
|
+
let inThrottle;
|
|
30
|
+
return function executedFunction(...args) {
|
|
31
|
+
if (!inThrottle) {
|
|
32
|
+
func(...args);
|
|
33
|
+
inThrottle = true;
|
|
34
|
+
setTimeout(() => inThrottle = false, limit);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Format date to readable string
|
|
41
|
+
* @param {Date|string} date - Date to format
|
|
42
|
+
* @param {object} options - Intl.DateTimeFormat options
|
|
43
|
+
*/
|
|
44
|
+
export function formatDate(date: Date | string, options: Intl.DateTimeFormatOptions = {}): string {
|
|
45
|
+
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
46
|
+
year: 'numeric' as const,
|
|
47
|
+
month: 'short' as const,
|
|
48
|
+
day: 'numeric' as const,
|
|
49
|
+
};
|
|
50
|
+
return new Intl.DateTimeFormat('en-US', { ...defaultOptions, ...options })
|
|
51
|
+
.format(new Date(date));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format number with thousands separator
|
|
56
|
+
* @param {number} num - Number to format
|
|
57
|
+
*/
|
|
58
|
+
export function formatNumber(num) {
|
|
59
|
+
return new Intl.NumberFormat('en-US').format(num);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format currency
|
|
64
|
+
* @param {number} amount - Amount to format
|
|
65
|
+
* @param {string} currency - Currency code (default: USD)
|
|
66
|
+
*/
|
|
67
|
+
export function formatCurrency(amount, currency = 'USD') {
|
|
68
|
+
return new Intl.NumberFormat('en-US', {
|
|
69
|
+
style: 'currency',
|
|
70
|
+
currency,
|
|
71
|
+
}).format(amount);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Truncate string to max length
|
|
76
|
+
* @param {string} str - String to truncate
|
|
77
|
+
* @param {number} maxLength - Maximum length
|
|
78
|
+
*/
|
|
79
|
+
export function truncate(str, maxLength = 50) {
|
|
80
|
+
if (str.length <= maxLength) return str;
|
|
81
|
+
return str.slice(0, maxLength - 3) + '...';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Generate unique ID
|
|
86
|
+
*/
|
|
87
|
+
export function generateId() {
|
|
88
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Deep clone object
|
|
93
|
+
* @param {object} obj - Object to clone
|
|
94
|
+
*/
|
|
95
|
+
export function deepClone(obj) {
|
|
96
|
+
return JSON.parse(JSON.stringify(obj));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check if object is empty
|
|
101
|
+
* @param {object} obj - Object to check
|
|
102
|
+
*/
|
|
103
|
+
export function isEmpty(obj) {
|
|
104
|
+
return Object.keys(obj).length === 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sleep/delay function
|
|
109
|
+
* @param {number} ms - Milliseconds to sleep
|
|
110
|
+
*/
|
|
111
|
+
export function sleep(ms) {
|
|
112
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sanitize HTML string to prevent XSS
|
|
117
|
+
* @param {string} str - String to sanitize
|
|
118
|
+
*/
|
|
119
|
+
export function sanitizeHTML(str) {
|
|
120
|
+
const temp = document.createElement('div');
|
|
121
|
+
temp.textContent = str;
|
|
122
|
+
return temp.innerHTML;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Parse query string to object
|
|
127
|
+
* @param {string} queryString - Query string (with or without ?)
|
|
128
|
+
*/
|
|
129
|
+
export function parseQueryString(queryString) {
|
|
130
|
+
const params = new URLSearchParams(queryString);
|
|
131
|
+
const result = {};
|
|
132
|
+
for (const [key, value] of params) {
|
|
133
|
+
result[key] = value;
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Build query string from object
|
|
140
|
+
* @param {object} params - Parameters object
|
|
141
|
+
*/
|
|
142
|
+
export function buildQueryString(params) {
|
|
143
|
+
return new URLSearchParams(params).toString();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Validate email format
|
|
148
|
+
* @param {string} email - Email to validate
|
|
149
|
+
*/
|
|
150
|
+
export function isValidEmail(email) {
|
|
151
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
152
|
+
return emailRegex.test(email);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Copy text to clipboard
|
|
157
|
+
* @param {string} text - Text to copy
|
|
158
|
+
*/
|
|
159
|
+
export async function copyToClipboard(text) {
|
|
160
|
+
try {
|
|
161
|
+
await navigator.clipboard.writeText(text);
|
|
162
|
+
return true;
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('Failed to copy:', err);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if element is in viewport
|
|
171
|
+
* @param {HTMLElement} element - Element to check
|
|
172
|
+
*/
|
|
173
|
+
export function isInViewport(element) {
|
|
174
|
+
const rect = element.getBoundingClientRect();
|
|
175
|
+
return (
|
|
176
|
+
rect.top >= 0 &&
|
|
177
|
+
rect.left >= 0 &&
|
|
178
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
179
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Scroll element into view smoothly
|
|
185
|
+
* @param {HTMLElement|string} elementOrSelector - Element or selector
|
|
186
|
+
*/
|
|
187
|
+
export function scrollToElement(elementOrSelector) {
|
|
188
|
+
const element = typeof elementOrSelector === 'string'
|
|
189
|
+
? document.querySelector(elementOrSelector)
|
|
190
|
+
: elementOrSelector;
|
|
191
|
+
|
|
192
|
+
if (element) {
|
|
193
|
+
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
194
|
+
}
|
|
195
|
+
}
|