main
ts 202 lines 5.38 KB
Raw
1 import type { Component } from "vue"
2 import type { ApiError, OsTypesFull, SafeAny } from "@/types/common.d"
3 import { isMobile as detectMobile } from "detect-touch-device"
4 import { md5 } from "js-md5"
5 import isDateObject from "lodash/isDate"
6 import _trim from "lodash/trim"
7 import { h } from "vue"
8 import Icon from "@/components/common/Icon.vue"
9 import dayjs from "./dayjs"
10
11 const TRAILING_DOT_REGEX = /\.$/
12
13 // Transform File Instance in base64 string
14 export function file2Base64(blob: Blob): Promise<string> {
15 return new Promise((resolve, reject) => {
16 const reader = new FileReader()
17 reader.readAsDataURL(blob)
18 reader.onload = () => resolve(reader.result as string)
19 reader.onerror = error => reject(error)
20 })
21 }
22
23 export function isEnvDev() {
24 return import.meta.env.DEV
25 }
26 export function isEnvTest() {
27 return import.meta.env.MODE === "test"
28 }
29 export function isEnvProd() {
30 return import.meta.env.PROD
31 }
32
33 export function isMobile() {
34 return detectMobile
35 }
36
37 const URL_PATTERN = /^https?:\/\//i
38
39 export function isUrlLike(text: string) {
40 return URL_PATTERN.test(text)
41 }
42
43 export function renderIcon(icon: Component | string) {
44 if (typeof icon === "string") {
45 return () => h(Icon, { name: icon })
46 } else {
47 return () => h(Icon, null, { default: () => h(icon) })
48 }
49 }
50
51 export function iconFromOs(os: string): string {
52 switch (getOS(os).toLowerCase()) {
53 case "windows":
54 return "mdi:microsoft"
55 case "macos":
56 return "mdi:apple"
57 case "linux":
58 case "unix":
59 return "mdi:linux"
60 default:
61 return "mdi:help-box"
62 }
63 }
64
65 export function getOS(os: string): OsTypesFull {
66 const test = os.toLowerCase()
67 if (test.includes("mac") || test.includes("darwin") || test.includes("apple")) {
68 return "MacOS"
69 }
70 if (test.includes("win") || test.includes("microsoft")) {
71 return "Windows"
72 }
73 if (
74 test.includes("linux") ||
75 test.includes("ubuntu") ||
76 test.includes("unix") ||
77 test.includes("x11") ||
78 test.includes("debian") ||
79 test.includes("centos")
80 ) {
81 return "Linux"
82 }
83
84 return "Unknown"
85 }
86
87 export function getNavigatorOS(): OsTypesFull {
88 let os: OsTypesFull = "Unknown"
89 if (navigator.userAgent.includes("Win")) os = "Windows"
90 if (navigator.userAgent.includes("Mac")) os = "MacOS"
91 if (navigator.userAgent.includes("X11")) os = "UNIX"
92 if (navigator.userAgent.includes("Linux")) os = "Linux"
93
94 return os
95 }
96
97 export function delay(t: number) {
98 return new Promise(res => setTimeout(res, t))
99 }
100
101 export function hashMD5(text: number | string) {
102 return md5(text.toString())
103 }
104
105 export function price(
106 amount: number,
107 options: { currency?: "USD" | "EUR"; splitDecimal?: boolean } = { currency: "USD", splitDecimal: true }
108 ) {
109 let symbol = ""
110 switch (options.currency) {
111 case "USD":
112 symbol = "$"
113 break
114 case "EUR":
115 symbol = ""
116 break
117 }
118
119 const price = options.splitDecimal ? (amount / 100).toFixed(2) : amount
120
121 return `${symbol}${price}`
122 }
123
124 export function getBaseUrl() {
125 return _trim(import.meta.env.VITE_API_URL, "/")
126 }
127
128 export function getNameInitials(name: string, cap?: number) {
129 let initials = name.slice(0, 2)
130
131 if (name.includes(" ")) {
132 initials = name
133 .split(" ")
134 .map(chunk => chunk[0])
135 .join("")
136 }
137
138 return (cap ? initials.slice(0, cap) : initials).toUpperCase()
139 }
140
141 export function getAvatar(params: { seed: string; text?: string; size?: number; format?: "png" | "svg" }) {
142 const format: "png" | "svg" = params.text ? "svg" : params.format || "svg"
143
144 return `https://avatar.vercel.sh/${params.seed}.${format}?text=${params.text || ""}&size=${params.size || 32}`
145 }
146
147 const NUMERIC_TIMESTAMP_REGEX = /^\d{10,}$/
148
149 export function isDate(val?: SafeAny): boolean {
150 if (val === undefined || val === null || val === "") return false
151
152 if (isDateObject(val)) return true
153
154 const strVal = String(val)
155
156 // Check for numeric timestamps (seconds, ms or µs)
157 // We enforce a minimum length of 10 digits to avoid false positives like "188" or "2"
158 if (NUMERIC_TIMESTAMP_REGEX.test(strVal)) {
159 const num = Number.parseInt(strVal)
160 // Handle ms (13 digits) or µs (16+ digits) by normalizing to ms
161 const date = strVal.length >= 13 ? dayjs(num / 10 ** (strVal.length - 13)) : dayjs(num * 1000)
162 return date.isValid()
163 }
164
165 // For ISO strings and other complex formats, we use a hybrid approach
166 // 1. Check for ISO-like strings (containing T and possibly Z or +/- offset)
167 if (strVal.includes("T")) {
168 // dayjs() parser is quite robust for ISO 8601 even without explicit format
169 return dayjs(strVal).isValid()
170 }
171
172 // 2. Strict parsing for regional formats
173 const regionalFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "DD-MM-YYYY", "MM-DD-YYYY", "YYYY-MM-DD", "YYYY-DD-MM"]
174
175 return dayjs(strVal, regionalFormats, true).isValid()
176 }
177
178 export function formatCompactNumber(value: number | null | undefined): string {
179 if (value == null) return ""
180 if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
181 if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`
182 return value.toLocaleString()
183 }
184
185 export function getApiErrorMessage(err: ApiError): string {
186 const axiosMessage = err.message
187 const message = err.response?.data?.message
188 const detail = err.response?.data?.detail
189
190 // Handle string detail
191 if (detail && typeof detail === "string") {
192 return `${detail.replace(TRAILING_DOT_REGEX, "")}.`
193 }
194
195 // Handle string message
196 if (message && typeof message === "string") {
197 return `${message.replace(TRAILING_DOT_REGEX, "")}.`
198 }
199
200 // Fallback to axios message
201 return `${axiosMessage.replace(TRAILING_DOT_REGEX, "")}.`
202 }