Skip to content

Commit 3dfcecc

Browse files
AchoArnoldCopilot
andcommitted
feat(web): port utilities, types, and composables
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b0e08a8 commit 3dfcecc

12 files changed

Lines changed: 1126 additions & 0 deletions

File tree

web/app/composables/useApi.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
let authToken: string | null = null;
2+
let apiKey: string | null = null;
3+
4+
export function setAuthHeader(token: string | null) {
5+
authToken = token;
6+
}
7+
8+
export function setApiKey(key: string | null) {
9+
apiKey = key;
10+
}
11+
12+
export function useApi() {
13+
const config = useRuntimeConfig();
14+
const baseURL = config.public.apiBaseUrl as string;
15+
16+
const apiFetch = $fetch.create({
17+
baseURL,
18+
headers: {
19+
"X-Client-Version": "web",
20+
},
21+
onRequest({ options }) {
22+
const headers = (options.headers ||= {}) as Record<string, string>;
23+
if (authToken) {
24+
headers.Authorization = `Bearer ${authToken}`;
25+
}
26+
if (apiKey) {
27+
headers["x-api-key"] = apiKey;
28+
}
29+
},
30+
});
31+
32+
return { apiFetch, setAuthHeader, setApiKey };
33+
}

web/app/composables/useFilters.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
formatPhoneNumber,
3+
phoneCountry,
4+
formatTimestamp,
5+
formatMoney,
6+
formatDecimal,
7+
formatBillingPeriod,
8+
humanizeTime,
9+
} from "~/utils/filters";
10+
import { capitalize } from "~/utils/capitalize";
11+
12+
export function useFilters() {
13+
return {
14+
formatPhoneNumber,
15+
phoneCountry,
16+
formatTimestamp,
17+
formatMoney,
18+
formatDecimal,
19+
formatBillingPeriod,
20+
humanizeTime,
21+
capitalize,
22+
};
23+
}

web/app/utils/bag.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
export default class Bag<T> {
2+
private items = new Map<string, Array<T>>();
3+
4+
serialize(): Record<string, Array<T>> {
5+
const result: Record<string, Array<T>> = {};
6+
this.items.forEach((value: T[], key) => {
7+
result[key] = value;
8+
});
9+
return result;
10+
}
11+
12+
static fromObject<T>(items: Record<string, Array<T>>): Bag<T> {
13+
const result = new Bag<T>();
14+
Object.keys(items).forEach((key) => {
15+
result.addMany(key, items[key]);
16+
});
17+
return result;
18+
}
19+
20+
add(key: string, value: T): this {
21+
let messages: Array<T> | undefined = this.items.get(key);
22+
if (messages === undefined) {
23+
messages = [];
24+
}
25+
26+
if (!messages.includes(value)) {
27+
messages.push(value);
28+
}
29+
30+
this.items.set(key, messages);
31+
return this;
32+
}
33+
34+
addMany(key: string, values: Array<T>): this {
35+
values.forEach((value: T) => {
36+
this.add(key, value);
37+
});
38+
return this;
39+
}
40+
41+
has(key: string): boolean {
42+
return this.items.has(key);
43+
}
44+
45+
first(key: string): T | undefined {
46+
if (this.has(key)) {
47+
return this.get(key)[0] ?? undefined;
48+
}
49+
return undefined;
50+
}
51+
52+
get(key: string): Array<T> {
53+
const result = this.items.get(key);
54+
if (result === undefined) {
55+
return [];
56+
}
57+
return result;
58+
}
59+
60+
size(): number {
61+
return this.items.size;
62+
}
63+
}

web/app/utils/capitalize.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function capitalize(value: string | null): string {
2+
if (!value) {
3+
return "";
4+
}
5+
return value.charAt(0).toUpperCase() + value.slice(1);
6+
}

web/app/utils/errors.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Bag from "~/utils/bag";
2+
import { capitalize } from "~/utils/capitalize";
3+
4+
export class ErrorMessages extends Bag<string> {}
5+
6+
const sanitize = (key: string, values: Array<string>): Array<string> => {
7+
return values.map((value: string) => {
8+
return capitalize(
9+
value
10+
.split(key)
11+
.join(key.replace("_", " "))
12+
.split("_")
13+
.join(" ")
14+
.split("-")
15+
.join(" ")
16+
.split(" char")
17+
.join(" character")
18+
.split(" field ")
19+
.join(" "),
20+
);
21+
});
22+
};
23+
24+
interface AxiosLikeError {
25+
response?: {
26+
data?: { data?: Record<string, string[]> };
27+
status?: number;
28+
};
29+
}
30+
31+
export const getErrorMessages = (error: AxiosLikeError): ErrorMessages => {
32+
const errors = new ErrorMessages();
33+
if (
34+
error === null ||
35+
typeof error.response?.data?.data !== "object" ||
36+
error.response?.data?.data === null ||
37+
error.response?.status !== 422
38+
) {
39+
return errors;
40+
}
41+
42+
Object.keys(error.response.data.data).forEach((key: string) => {
43+
errors.addMany(key, sanitize(key, error.response!.data!.data![key]));
44+
});
45+
46+
return errors;
47+
};

web/app/utils/filters.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { intervalToDuration, formatDuration } from "date-fns";
2+
import { parsePhoneNumber, isValidPhoneNumber } from "libphonenumber-js";
3+
4+
export function formatPhoneNumber(value: string): string {
5+
if (!isValidPhoneNumber(value)) {
6+
return value;
7+
}
8+
const phoneNumber = parsePhoneNumber(value);
9+
if (phoneNumber) {
10+
return phoneNumber.formatInternational();
11+
}
12+
return value;
13+
}
14+
15+
export function phoneCountry(value: string): string {
16+
const phoneNumber = parsePhoneNumber(value);
17+
if (phoneNumber && phoneNumber.country) {
18+
const regionNames = new Intl.DisplayNames(undefined, { type: "region" });
19+
return regionNames.of(phoneNumber.country) ?? "Earth";
20+
}
21+
return "Earth";
22+
}
23+
24+
export function formatTimestamp(value: string): string {
25+
return new Date(value).toLocaleString();
26+
}
27+
28+
export function formatMoney(value: string | number): string {
29+
return new Intl.NumberFormat("en-US", {
30+
style: "currency",
31+
currency: "USD",
32+
}).format(typeof value === "string" ? parseInt(value) : value);
33+
}
34+
35+
export function formatDecimal(value: string | number): string {
36+
return new Intl.NumberFormat("en-US", {
37+
style: "decimal",
38+
}).format(typeof value === "string" ? parseInt(value) : value);
39+
}
40+
41+
export function formatBillingPeriod(value: string): string {
42+
return new Date(value).toLocaleDateString("en-US", {
43+
year: "numeric",
44+
month: "long",
45+
});
46+
}
47+
48+
export function humanizeTime(value: string): string {
49+
const durations = intervalToDuration({
50+
start: new Date(),
51+
end: new Date(value),
52+
});
53+
return formatDuration(durations);
54+
}

0 commit comments

Comments
 (0)