quasar-ui-danx 0.4.91 → 0.4.93
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/dist/danx.es.js +7369 -6609
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +90 -90
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +123 -136
- package/src/components/Utility/Files/CarouselHeader.vue +80 -0
- package/src/components/Utility/Files/FilePreview.vue +76 -21
- package/src/components/Utility/Files/FileRenderer.vue +111 -0
- package/src/components/Utility/Files/ThumbnailStrip.vue +64 -0
- package/src/components/Utility/Files/TranscodeNavigator.vue +175 -0
- package/src/components/Utility/Files/VirtualCarousel.vue +237 -0
- package/src/components/Utility/Files/index.ts +5 -0
- package/src/components/Utility/Widgets/LabelPillWidget.vue +10 -0
- package/src/composables/index.ts +4 -0
- package/src/composables/useFileNavigation.ts +129 -0
- package/src/composables/useKeyboardNavigation.ts +58 -0
- package/src/composables/useThumbnailScroll.ts +43 -0
- package/src/composables/useVirtualCarousel.ts +93 -0
- package/src/helpers/filePreviewHelpers.ts +107 -0
- package/src/helpers/formats/datetime.ts +285 -0
- package/src/helpers/formats/index.ts +4 -0
- package/src/helpers/formats/numbers.ts +127 -0
- package/src/helpers/formats/parsers.ts +74 -0
- package/src/helpers/formats/strings.ts +65 -0
- package/src/helpers/formats.ts +1 -489
- package/src/helpers/index.ts +1 -0
- package/src/types/files.d.ts +38 -0
- package/src/types/widgets.d.ts +1 -1
- package/src/vue-plugin.ts +1 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { nextTick, Ref, watch } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface UseThumbnailScrollOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Container element that holds the thumbnails
|
|
6
|
+
*/
|
|
7
|
+
containerRef: Ref<HTMLElement | null>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Current active index
|
|
11
|
+
*/
|
|
12
|
+
currentIndex: Ref<number>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optional CSS selector for thumbnail elements (defaults to '.thumbnail')
|
|
16
|
+
*/
|
|
17
|
+
thumbnailSelector?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Composable for auto-scrolling thumbnails into view
|
|
22
|
+
* Watches the current index and scrolls the active thumbnail into the visible area
|
|
23
|
+
*/
|
|
24
|
+
export function useThumbnailScroll(options: UseThumbnailScrollOptions) {
|
|
25
|
+
const { containerRef, currentIndex, thumbnailSelector = ".thumbnail" } = options;
|
|
26
|
+
|
|
27
|
+
watch(currentIndex, (newIndex) => {
|
|
28
|
+
nextTick(() => {
|
|
29
|
+
const thumbnails = containerRef.value?.querySelectorAll(thumbnailSelector);
|
|
30
|
+
const activeThumbnail = thumbnails?.[newIndex] as HTMLElement;
|
|
31
|
+
|
|
32
|
+
if (activeThumbnail && containerRef.value) {
|
|
33
|
+
activeThumbnail.scrollIntoView({
|
|
34
|
+
behavior: "smooth",
|
|
35
|
+
block: "nearest",
|
|
36
|
+
inline: "center"
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { computed, Ref, shallowRef, watch } from "vue";
|
|
2
|
+
import { UploadedFile, VirtualCarouselSlide } from "../types";
|
|
3
|
+
|
|
4
|
+
const BUFFER_SIZE = 2; // Render current slide ± 2 slides
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Composable for managing virtual carousel rendering
|
|
8
|
+
* Only renders slides within buffer window for performance with large file sets
|
|
9
|
+
*/
|
|
10
|
+
export function useVirtualCarousel(
|
|
11
|
+
files: Ref<UploadedFile[]>,
|
|
12
|
+
currentIndex: Ref<number>
|
|
13
|
+
) {
|
|
14
|
+
// Shallow ref to avoid deep reactivity for performance
|
|
15
|
+
const visibleSlides = shallowRef<VirtualCarouselSlide[]>([]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Calculate which slides should be visible based on current index
|
|
19
|
+
*/
|
|
20
|
+
const visibleIndices = computed(() => {
|
|
21
|
+
const start = Math.max(0, currentIndex.value - BUFFER_SIZE);
|
|
22
|
+
const end = Math.min(files.value.length - 1, currentIndex.value + BUFFER_SIZE);
|
|
23
|
+
const indices: number[] = [];
|
|
24
|
+
|
|
25
|
+
for (let i = start; i <= end; i++) {
|
|
26
|
+
indices.push(i);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return indices;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Update visible slides based on current index
|
|
34
|
+
*/
|
|
35
|
+
function updateVisibleSlides() {
|
|
36
|
+
const indices = visibleIndices.value;
|
|
37
|
+
const newSlides: VirtualCarouselSlide[] = [];
|
|
38
|
+
|
|
39
|
+
for (const index of indices) {
|
|
40
|
+
if (index >= 0 && index < files.value.length) {
|
|
41
|
+
newSlides.push({
|
|
42
|
+
file: files.value[index],
|
|
43
|
+
index,
|
|
44
|
+
isActive: index === currentIndex.value,
|
|
45
|
+
isVisible: true
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
visibleSlides.value = newSlides;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a slide at given index is visible
|
|
55
|
+
*/
|
|
56
|
+
function isSlideVisible(index: number): boolean {
|
|
57
|
+
return visibleIndices.value.includes(index);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get slide by index (returns null if not visible)
|
|
62
|
+
*/
|
|
63
|
+
function getSlide(index: number): VirtualCarouselSlide | null {
|
|
64
|
+
return visibleSlides.value.find(s => s.index === index) || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get slides that should be preloaded (visible + buffer)
|
|
69
|
+
*/
|
|
70
|
+
const preloadIndices = computed(() => {
|
|
71
|
+
const start = Math.max(0, currentIndex.value - BUFFER_SIZE - 1);
|
|
72
|
+
const end = Math.min(files.value.length - 1, currentIndex.value + BUFFER_SIZE + 1);
|
|
73
|
+
const indices: number[] = [];
|
|
74
|
+
|
|
75
|
+
for (let i = start; i <= end; i++) {
|
|
76
|
+
indices.push(i);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return indices;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Watch for changes to update visible slides
|
|
83
|
+
watch([currentIndex, files], updateVisibleSlides, { immediate: true });
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
visibleSlides,
|
|
87
|
+
visibleIndices,
|
|
88
|
+
preloadIndices,
|
|
89
|
+
isSlideVisible,
|
|
90
|
+
getSlide,
|
|
91
|
+
updateVisibleSlides
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { UploadedFile } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Placeholder constants for file preview
|
|
5
|
+
*/
|
|
6
|
+
export const FILE_PLACEHOLDERS = {
|
|
7
|
+
IMAGE: "https://placehold.co/64x64?text=?",
|
|
8
|
+
VIDEO_SVG: `data:image/svg+xml;base64,${btoa(
|
|
9
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M8 5v14l11-7z"/></svg>'
|
|
10
|
+
)}`
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MIME type regex patterns for file type detection
|
|
15
|
+
*/
|
|
16
|
+
export const MIME_TYPES = {
|
|
17
|
+
IMAGE: /^image\//,
|
|
18
|
+
VIDEO: /^video\//,
|
|
19
|
+
TEXT: /^text\//,
|
|
20
|
+
PDF: /^application\/pdf/
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if file is an image
|
|
25
|
+
*/
|
|
26
|
+
export function isImage(file: UploadedFile): boolean {
|
|
27
|
+
const mimeType = getMimeType(file);
|
|
28
|
+
return MIME_TYPES.IMAGE.test(mimeType);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if file is a video
|
|
33
|
+
*/
|
|
34
|
+
export function isVideo(file: UploadedFile): boolean {
|
|
35
|
+
const mimeType = getMimeType(file);
|
|
36
|
+
return MIME_TYPES.VIDEO.test(mimeType);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if file is a text file
|
|
41
|
+
*/
|
|
42
|
+
export function isText(file: UploadedFile): boolean {
|
|
43
|
+
const mimeType = getMimeType(file);
|
|
44
|
+
return MIME_TYPES.TEXT.test(mimeType);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if file is a PDF
|
|
49
|
+
*/
|
|
50
|
+
export function isPdf(file: UploadedFile): boolean {
|
|
51
|
+
const mimeType = getMimeType(file);
|
|
52
|
+
return MIME_TYPES.PDF.test(mimeType);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the MIME type from a file
|
|
57
|
+
*/
|
|
58
|
+
export function getMimeType(file: UploadedFile): string {
|
|
59
|
+
return file.mime || file.type || "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the file extension from a filename
|
|
64
|
+
*/
|
|
65
|
+
export function getFileExtension(filename: string): string {
|
|
66
|
+
return filename.split(".").pop()?.toLowerCase() || "";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the preview URL for a file
|
|
71
|
+
* Priority: optimized > blobUrl > url > thumb > empty string
|
|
72
|
+
*/
|
|
73
|
+
export function getPreviewUrl(file: UploadedFile): string {
|
|
74
|
+
if (file.optimized?.url) {
|
|
75
|
+
return file.optimized.url;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isImage(file)) {
|
|
79
|
+
return file.blobUrl || file.url || "";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return file.thumb?.url || "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the thumbnail URL for a file
|
|
87
|
+
* For videos without thumbs, returns a play icon SVG
|
|
88
|
+
*/
|
|
89
|
+
export function getThumbUrl(file: UploadedFile): string {
|
|
90
|
+
if (file.thumb?.url) {
|
|
91
|
+
return file.thumb.url;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (isVideo(file)) {
|
|
95
|
+
return FILE_PLACEHOLDERS.VIDEO_SVG;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return getPreviewUrl(file) || FILE_PLACEHOLDERS.IMAGE;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get the optimized URL for a file (used for preview)
|
|
103
|
+
* Priority: optimized > blobUrl > url
|
|
104
|
+
*/
|
|
105
|
+
export function getOptimizedUrl(file: UploadedFile): string {
|
|
106
|
+
return file.optimized?.url || file.blobUrl || file.url || "";
|
|
107
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { DateTime, IANAZone } from "luxon";
|
|
2
|
+
import { fDateOptions } from "../../types";
|
|
3
|
+
|
|
4
|
+
const SERVER_TZ = new IANAZone("America/Chicago");
|
|
5
|
+
|
|
6
|
+
export { DateTime, SERVER_TZ };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a date string from the server's time zone to the user's time zone.
|
|
10
|
+
* @param {String} dateTimeString
|
|
11
|
+
* @returns {DateTime}
|
|
12
|
+
*/
|
|
13
|
+
export function localizedDateTime(dateTimeString: string) {
|
|
14
|
+
dateTimeString = dateTimeString?.replace("T", " ");
|
|
15
|
+
// noinspection JSCheckFunctionSignatures
|
|
16
|
+
return DateTime.fromSQL(dateTimeString, { zone: SERVER_TZ }).setZone("local");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts a date string from the user's time zone to the server's time zone.
|
|
21
|
+
* @param dateTimeString
|
|
22
|
+
* @returns {DateTime}
|
|
23
|
+
*/
|
|
24
|
+
export function remoteDateTime(dateTimeString: string) {
|
|
25
|
+
dateTimeString = dateTimeString?.replace("T", " ");
|
|
26
|
+
// noinspection JSCheckFunctionSignatures
|
|
27
|
+
return DateTime.fromSQL(dateTimeString, { zone: "local" }).setZone(SERVER_TZ);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Formats a Luxon DateTime object into a Quasar formatted date string
|
|
32
|
+
* @param date
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
export function fQDate(date: string) {
|
|
36
|
+
return fDate(date, { format: "yyyy/MM/dd" });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* @param {String} dateTimeString
|
|
42
|
+
* @param options
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function fLocalizedDateTime(dateTimeString: string, options = {}) {
|
|
46
|
+
return fDateTime(localizedDateTime(dateTimeString), options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Formats a date/time object or string into a human-readable format
|
|
51
|
+
*
|
|
52
|
+
* @param {String|Object} dateTime
|
|
53
|
+
* @param format
|
|
54
|
+
* @param {String|null} empty
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
export function fDateTime(
|
|
58
|
+
dateTime: string | DateTime | null = null,
|
|
59
|
+
{ format = "M/d/yy h:mma", empty = "- -" }: fDateOptions = {}
|
|
60
|
+
) {
|
|
61
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format).toLowerCase();
|
|
62
|
+
return formatted || empty;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function fDateTimeMs(
|
|
66
|
+
dateTime: string | DateTime | null = null,
|
|
67
|
+
{ empty = "- -" }: fDateOptions = {}
|
|
68
|
+
) {
|
|
69
|
+
const formatted = parseDateTime(dateTime)?.toFormat("M/d/yy H:mm:ss.SSS").toLowerCase();
|
|
70
|
+
return formatted || empty;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Formats a date/time object or string into the best format for DB input
|
|
75
|
+
* @param dateTime
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function dbDateTime(dateTime: string | DateTime | null = null) {
|
|
79
|
+
return fDateTime(dateTime, { format: "yyyy-MM-dd HH:mm:ss", empty: undefined });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Formats a date object or string into a human-readable format
|
|
84
|
+
* @param {String|Object} dateTime
|
|
85
|
+
* @param {String|null} empty
|
|
86
|
+
* @param format
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function fDate(dateTime: string | DateTime | null, { empty = "--", format = "M/d/yy" }: fDateOptions = {}) {
|
|
90
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format || "M/d/yy");
|
|
91
|
+
return formatted || empty;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parses a date string into a Luxon DateTime object
|
|
97
|
+
*/
|
|
98
|
+
export function parseDateTime(dateTime: string | DateTime | number | null): DateTime<boolean> | null {
|
|
99
|
+
if (typeof dateTime === "number") {
|
|
100
|
+
return DateTime.fromMillis(dateTime as number);
|
|
101
|
+
}
|
|
102
|
+
if (typeof dateTime === "string") {
|
|
103
|
+
return parseGenericDateTime(dateTime);
|
|
104
|
+
}
|
|
105
|
+
return dateTime as DateTime<boolean> || DateTime.fromSQL("0000-00-00 00:00:00");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parses a SQL formatted date string into a Luxon DateTime object
|
|
110
|
+
*/
|
|
111
|
+
export function parseSqlDateTime(dateTime: string) {
|
|
112
|
+
const parsed = DateTime.fromSQL(dateTime.replace("T", " ").replace(/\//g, "-"));
|
|
113
|
+
return parsed.isValid ? parsed : null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parses a Quasar formatted date string into a Luxon DateTime object
|
|
118
|
+
*/
|
|
119
|
+
export function parseQDate(date: string, format = "yyyy/MM/dd"): DateTime<boolean> | null {
|
|
120
|
+
const parsed = DateTime.fromFormat(date, format);
|
|
121
|
+
return parsed.isValid ? parsed : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parses a Quasar formatted date/time string into a Luxon DateTime object
|
|
126
|
+
*/
|
|
127
|
+
export function parseQDateTime(date: string, format = "yyyy/MM/dd HH:mm:ss"): DateTime<boolean> | null {
|
|
128
|
+
const parsed = DateTime.fromFormat(date, format);
|
|
129
|
+
return parsed.isValid ? parsed : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parses a date string in various formats into a Luxon DateTime object.
|
|
134
|
+
* Tries a list of common formats until one works.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} dateTimeString - The date string to parse.
|
|
137
|
+
* @param {string} [defaultZone="local"] - The default time zone to use if not specified.
|
|
138
|
+
* @returns {DateTime | null} - A Luxon DateTime object if parsing succeeds, otherwise null.
|
|
139
|
+
*/
|
|
140
|
+
export function parseGenericDateTime(dateTimeString: string, defaultZone = "local"): DateTime | null {
|
|
141
|
+
if (!dateTimeString) return null;
|
|
142
|
+
|
|
143
|
+
const formats = [
|
|
144
|
+
"yyyy-MM-dd", // ISO date
|
|
145
|
+
"yyyy-MM-dd HH:mm:ss", // ISO date with time
|
|
146
|
+
"MM/dd/yyyy", // US-style date
|
|
147
|
+
"dd/MM/yyyy", // European-style date
|
|
148
|
+
"MM/dd/yy", // US short date
|
|
149
|
+
"dd/MM/yy", // European short date
|
|
150
|
+
"yyyy/MM/dd", // Alternative ISO
|
|
151
|
+
"MM-dd-yyyy", // US with dashes
|
|
152
|
+
"dd-MM-yyyy", // European with dashes
|
|
153
|
+
"M/d/yyyy", // US date without leading zeros
|
|
154
|
+
"d/M/yyyy", // European date without leading zeros
|
|
155
|
+
"yyyyMMdd" // Compact ISO
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
for (const format of formats) {
|
|
159
|
+
const parsed = DateTime.fromFormat(dateTimeString, format, { zone: defaultZone });
|
|
160
|
+
if (parsed.isValid) {
|
|
161
|
+
return parsed;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback to ISO parsing for strings like "2022-11-18T10:10:10Z"
|
|
166
|
+
const isoParsed = DateTime.fromISO(dateTimeString, { zone: defaultZone });
|
|
167
|
+
if (isoParsed.isValid) {
|
|
168
|
+
return isoParsed;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Fallback to SQL parsing for strings like "2022-11-18 10:10:10"
|
|
172
|
+
const sqlParsed = DateTime.fromSQL(dateTimeString, { zone: defaultZone });
|
|
173
|
+
if (sqlParsed.isValid) {
|
|
174
|
+
return sqlParsed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Formats a number of seconds into Hours / Minutes / Seconds or just Minutes and Seconds
|
|
182
|
+
*
|
|
183
|
+
* @param second
|
|
184
|
+
* @returns {string}
|
|
185
|
+
*/
|
|
186
|
+
export function fSecondsToTime(second: number) {
|
|
187
|
+
const time = DateTime.now().setZone("UTC").startOf("year").set({ second });
|
|
188
|
+
const hours = Math.floor(second / 3600);
|
|
189
|
+
return (hours ? hours + ":" : "") + time.toFormat("mm:ss");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Formats a number of seconds into a duration string in 00h 00m 00s format
|
|
194
|
+
*/
|
|
195
|
+
export function fSecondsToDuration(seconds: number) {
|
|
196
|
+
const hours = Math.floor(seconds / 3600);
|
|
197
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
198
|
+
const secs = Math.floor(seconds % 60);
|
|
199
|
+
return `${hours ? hours + "h " : ""}${minutes ? minutes + "m " : ""}${secs}s`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Formats a number of milliseconds into a duration string in 00h 00m 00s 000ms format
|
|
204
|
+
*/
|
|
205
|
+
export function fMillisecondsToDuration(milliseconds: number) {
|
|
206
|
+
const durStr = fSecondsToDuration(Math.floor(milliseconds / 1000));
|
|
207
|
+
return (durStr === "0s" ? "" : durStr) + ` ${Math.floor(milliseconds % 1000)}ms`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Formats a duration between two date strings in 00h 00m 00s format
|
|
213
|
+
*/
|
|
214
|
+
export function fDuration(start: string | number, end?: string | number) {
|
|
215
|
+
const endDateTime = end ? parseDateTime(end) : DateTime.now();
|
|
216
|
+
const diff = endDateTime?.diff(parseDateTime(start) || DateTime.now(), ["hours", "minutes", "seconds"]);
|
|
217
|
+
if (!diff?.isValid) {
|
|
218
|
+
return "-";
|
|
219
|
+
}
|
|
220
|
+
const totalSeconds = diff.as("seconds");
|
|
221
|
+
return fSecondsToDuration(totalSeconds);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Formats a date/time as a relative time string (e.g., "5 minutes ago", "yesterday")
|
|
226
|
+
* @param {String|DateTime|number} dateTime - The date/time to format
|
|
227
|
+
* @returns {string} - A human-readable relative time string
|
|
228
|
+
*/
|
|
229
|
+
export function fTimeAgo(dateTime: string | DateTime | number | null): string {
|
|
230
|
+
if (!dateTime) return "";
|
|
231
|
+
|
|
232
|
+
const date = parseDateTime(dateTime);
|
|
233
|
+
if (!date) return "";
|
|
234
|
+
|
|
235
|
+
const now = DateTime.now();
|
|
236
|
+
const diffTime = Math.abs(now.toMillis() - date.toMillis());
|
|
237
|
+
|
|
238
|
+
// Convert to different units
|
|
239
|
+
const diffSeconds = Math.floor(diffTime / 1000);
|
|
240
|
+
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
|
241
|
+
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
|
242
|
+
|
|
243
|
+
// Seconds
|
|
244
|
+
if (diffSeconds < 10) {
|
|
245
|
+
return "a few seconds ago";
|
|
246
|
+
} else if (diffSeconds < 60) {
|
|
247
|
+
return `${diffSeconds} seconds ago`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Minutes
|
|
251
|
+
if (diffMinutes === 1) {
|
|
252
|
+
return "a minute ago";
|
|
253
|
+
} else if (diffMinutes < 60) {
|
|
254
|
+
return `${diffMinutes} minutes ago`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Hours
|
|
258
|
+
if (diffHours === 1) {
|
|
259
|
+
return "an hour ago";
|
|
260
|
+
} else if (diffHours < 24) {
|
|
261
|
+
return `${diffHours} hours ago`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Calendar-based day logic
|
|
265
|
+
const today = now.startOf("day");
|
|
266
|
+
const authDay = date.startOf("day");
|
|
267
|
+
const daysDiff = Math.floor(today.diff(authDay, "days").days);
|
|
268
|
+
|
|
269
|
+
if (daysDiff === 0) {
|
|
270
|
+
return "today";
|
|
271
|
+
} else if (daysDiff === 1) {
|
|
272
|
+
return "yesterday";
|
|
273
|
+
} else if (daysDiff < 7) {
|
|
274
|
+
return `${daysDiff} days ago`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Weeks
|
|
278
|
+
if (daysDiff < 30) {
|
|
279
|
+
const weeks = Math.floor(daysDiff / 7);
|
|
280
|
+
return weeks === 1 ? "a week ago" : `${weeks} weeks ago`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 30+ days: show full date
|
|
284
|
+
return date.toLocaleString({ year: "numeric", month: "short", day: "numeric" });
|
|
285
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats an amount into USD currency format
|
|
3
|
+
*/
|
|
4
|
+
export function fCurrency(amount: number, options?: object) {
|
|
5
|
+
if (amount === null || amount === undefined || isNaN(amount)) {
|
|
6
|
+
return "$-";
|
|
7
|
+
}
|
|
8
|
+
return new Intl.NumberFormat("en-US", {
|
|
9
|
+
style: "currency",
|
|
10
|
+
currency: "USD",
|
|
11
|
+
...options
|
|
12
|
+
}).format(amount);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Formats an amount into USD currency format without cents
|
|
17
|
+
*/
|
|
18
|
+
export function fCurrencyNoCents(amount: number, options?: object) {
|
|
19
|
+
return fCurrency(amount, {
|
|
20
|
+
maximumFractionDigits: 0,
|
|
21
|
+
...options
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats a number into a human-readable format
|
|
27
|
+
*/
|
|
28
|
+
export function fNumber(number: number, options?: object) {
|
|
29
|
+
return new Intl.NumberFormat("en-US", options).format(number);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats a currency into a shorthand human-readable format (ie: $1.2M or $5K)
|
|
34
|
+
*/
|
|
35
|
+
export function fShortCurrency(value: string | number, options?: { round: boolean }) {
|
|
36
|
+
return "$" + fShortNumber(value, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Formats a number into a shorthand human-readable format (ie: 1.2M or 5K)
|
|
41
|
+
*/
|
|
42
|
+
export function fShortNumber(value: string | number, options?: { round: boolean }) {
|
|
43
|
+
if (value === "" || value === null || value === undefined) {
|
|
44
|
+
return "-";
|
|
45
|
+
}
|
|
46
|
+
const shorts = [
|
|
47
|
+
{ pow: 3, unit: "K" },
|
|
48
|
+
{ pow: 6, unit: "M" },
|
|
49
|
+
{ pow: 9, unit: "B" },
|
|
50
|
+
{ pow: 12, unit: "T" },
|
|
51
|
+
{ pow: 15, unit: "Q" }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
let n = Math.round(+value);
|
|
55
|
+
|
|
56
|
+
const short = shorts.find(({ pow }) => Math.pow(10, pow) < n && Math.pow(10, pow + 3) > n) || null;
|
|
57
|
+
|
|
58
|
+
if (short) {
|
|
59
|
+
n = n / Math.pow(10, short.pow);
|
|
60
|
+
return options?.round
|
|
61
|
+
? n + short.unit
|
|
62
|
+
: n.toFixed(n > 100 ? 0 : 1) + short.unit;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return n;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Formats a number into a human-readable size format (ie: 1.2MB or 5KB)
|
|
70
|
+
*/
|
|
71
|
+
export function fShortSize(value: string | number) {
|
|
72
|
+
const powers = [
|
|
73
|
+
{ pow: 0, unit: "B" },
|
|
74
|
+
{ pow: 10, unit: "KB" },
|
|
75
|
+
{ pow: 20, unit: "MB" },
|
|
76
|
+
{ pow: 30, unit: "GB" },
|
|
77
|
+
{ pow: 40, unit: "TB" },
|
|
78
|
+
{ pow: 50, unit: "PB" },
|
|
79
|
+
{ pow: 60, unit: "EB" },
|
|
80
|
+
{ pow: 70, unit: "ZB" },
|
|
81
|
+
{ pow: 80, unit: "YB" }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const n = Math.round(+value);
|
|
85
|
+
const power = powers.find((p, i) => {
|
|
86
|
+
const nextPower = powers[i + 1];
|
|
87
|
+
return !nextPower || n < Math.pow(2, nextPower.pow + 10);
|
|
88
|
+
}) || powers[powers.length - 1];
|
|
89
|
+
|
|
90
|
+
const div = Math.pow(2, power.pow);
|
|
91
|
+
|
|
92
|
+
return Math.round(n / div) + " " + power.unit;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function fBoolean(value?: boolean | string | any) {
|
|
96
|
+
switch (value) {
|
|
97
|
+
case "Yes":
|
|
98
|
+
case "No":
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (value === undefined || value === null) ? "-" : (value ? "Yes" : "No");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface FPercentOptions {
|
|
106
|
+
multiplier?: number,
|
|
107
|
+
maximumFractionDigits?: number,
|
|
108
|
+
NaN?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Formats a number into a percentage
|
|
113
|
+
* @param num
|
|
114
|
+
* @param options
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function fPercent(num: string | number, options: FPercentOptions = {}) {
|
|
118
|
+
options = { multiplier: 100, maximumFractionDigits: 1, NaN: "N/A", ...options };
|
|
119
|
+
|
|
120
|
+
num = parseFloat("" + num);
|
|
121
|
+
|
|
122
|
+
if (isNaN(num)) {
|
|
123
|
+
return options.NaN;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fNumber(num * (options.multiplier || 100), options) + "%";
|
|
127
|
+
}
|