init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export const capitalizeFirstLetter = (value: string): string =>
|
||||
value.charAt(0).toUpperCase() + value.slice(1);
|
||||
@@ -0,0 +1,3 @@
|
||||
import dayjs from '@/plugins/dayjs';
|
||||
|
||||
export const formatDate = (iso: string): string => dayjs(iso).format('YYYY-MM-DD HH:mm');
|
||||
@@ -0,0 +1,11 @@
|
||||
import Decimal from 'decimal.js';
|
||||
|
||||
export const formatFiatPrice = (price: number, unit: string): string => {
|
||||
const rounded = new Decimal(price).toDecimalPlaces(2);
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', { style: 'currency', currency: unit }).format(rounded.toNumber());
|
||||
} catch {
|
||||
return `${rounded.toString()} ${unit}`;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
|
||||
export const formatFileSize = (sizeBytes: number): string =>
|
||||
prettyBytes(sizeBytes, { binary: true, maximumFractionDigits: 0 });
|
||||
@@ -0,0 +1,3 @@
|
||||
import dayjs from '@/plugins/dayjs';
|
||||
|
||||
export const formatRelativeTimeAgo = (iso: string): string => dayjs(iso).fromNow();
|
||||
@@ -0,0 +1,2 @@
|
||||
export const getPaginationLastPage = (itemTotal: number, pageLimit: number): number =>
|
||||
Math.max(1, Math.ceil(itemTotal / pageLimit));
|
||||
@@ -0,0 +1 @@
|
||||
export const isSet = <T>(value: T | null | undefined): value is T => value !== null && value !== undefined;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { MoneroNetwork } from '@/types/moneroWallet/MoneroNetwork';
|
||||
|
||||
// Standard + subaddress, 95 chars. Excludes integrated (106 chars).
|
||||
// Prefix bytes: monero-project/monero src/cryptonote_config.h
|
||||
// Regex shape: https://gist.github.com/masflam/84477ca88842e245dc7a4cc61ce299e3
|
||||
const BASE58 = '[1-9A-HJ-NP-Za-km-z]';
|
||||
|
||||
const NETWORK_ADDRESS_PATTERNS: Record<MoneroNetwork, RegExp> = {
|
||||
mainnet: new RegExp(`^(?:4[1-9AB]|8[2-9ABC])${BASE58}{93}$`),
|
||||
stagenet: new RegExp(`^(?:5[1-9AB]|7[2-9AB])${BASE58}{93}$`)
|
||||
};
|
||||
|
||||
export const isMoneroStandardAddress = (value: unknown, network: MoneroNetwork): boolean => {
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return NETWORK_ADDRESS_PATTERNS[network].test(value.trim());
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { OrderFailureReason } from '@/types/order/OrderFailureReason';
|
||||
|
||||
const labels: Record<OrderFailureReason, string> = {
|
||||
[OrderFailureReason.StockUnavailable]: 'Stock unavailable',
|
||||
[OrderFailureReason.DiscountExhausted]: 'Discount exhausted'
|
||||
};
|
||||
|
||||
export const formatOrderFailureReason = (reason: OrderFailureReason): string => labels[reason] ?? reason;
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { InvoiceStatusLabel } from '@/types/payment/InvoiceStatusLabel';
|
||||
|
||||
export const resolveInvoiceStatusTagType = (
|
||||
statusLabel: InvoiceStatusLabel | null
|
||||
): 'success' | 'warning' | 'info' | 'danger' => {
|
||||
switch (statusLabel) {
|
||||
case 'Payment confirmed':
|
||||
return 'success';
|
||||
case 'Awaiting confirmations':
|
||||
case 'Partial payment received':
|
||||
return 'warning';
|
||||
case 'Payment expired':
|
||||
return 'danger';
|
||||
case 'Awaiting payment':
|
||||
return 'info';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { OrderStatus } from '@/types/order/OrderStatus';
|
||||
|
||||
export const resolveOrderStatusTagType = (status: OrderStatus): 'success' | 'warning' | 'info' | 'danger' => {
|
||||
switch (status) {
|
||||
case OrderStatus.Fulfilled:
|
||||
return 'success';
|
||||
case OrderStatus.Unfulfilled:
|
||||
return 'warning';
|
||||
case OrderStatus.Unfulfillable:
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { ProductVariant } from '@/types/product/ProductVariant';
|
||||
|
||||
export const compareProductVariants = (a: ProductVariant, b: ProductVariant): number =>
|
||||
a.sortOrder - b.sortOrder || a.createdAt.localeCompare(b.createdAt);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { DeliveryMode } from '@/types/product/DeliveryMode';
|
||||
|
||||
export const formatDeliveryMode = (deliveryMode: DeliveryMode): string => {
|
||||
if (deliveryMode === DeliveryMode.Manual) {
|
||||
return 'Manually fulfilled';
|
||||
}
|
||||
|
||||
return 'Auto-delivered';
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { UNTITLED_PRODUCT_TITLE } from '@/consts/untitledProductTitle';
|
||||
|
||||
export const getProductTitle = (title: string | undefined | null): string => title || UNTITLED_PRODUCT_TITLE;
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getProductTitle } from '@/utils/product/getProductTitle';
|
||||
|
||||
export const getVariantLabel = (variantTitle: string, productTitle: string | undefined | null): string =>
|
||||
`${getProductTitle(productTitle)} — ${variantTitle}`;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isAxiosError } from 'axios';
|
||||
|
||||
const getAxiosErrorResponseMessage = (data: unknown): string | null => {
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (typeof data !== 'object' || data === null || !('message' in data)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { message } = data;
|
||||
|
||||
if (typeof message === 'string') {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (Array.isArray(message)) {
|
||||
const texts = message.filter((item): item is string => typeof item === 'string');
|
||||
|
||||
if (texts.length === 1) {
|
||||
return texts[0];
|
||||
}
|
||||
|
||||
if (texts.length > 1) {
|
||||
return texts.join(', ');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resolveAxiosErrorMessage = (error: unknown, fallback: string): string => {
|
||||
if (!isAxiosError(error) || !error.response) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return getAxiosErrorResponseMessage(error.response.data) ?? fallback;
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { BuildUploadHintOptions } from '@/types/BuildUploadHintOptions';
|
||||
import { formatFileSize } from '@/utils/formatFileSize';
|
||||
|
||||
const MIME_LABEL: Record<string, string> = {
|
||||
'application/json': 'JSON',
|
||||
'application/pdf': 'PDF',
|
||||
'application/zip': 'ZIP',
|
||||
'image/gif': 'GIF',
|
||||
'image/jpeg': 'JPEG',
|
||||
'image/jpg': 'JPEG',
|
||||
'image/png': 'PNG',
|
||||
'image/vnd.microsoft.icon': 'ICO',
|
||||
'image/x-icon': 'ICO',
|
||||
'image/webp': 'WebP',
|
||||
'text/csv': 'CSV',
|
||||
'text/plain': 'plain text'
|
||||
};
|
||||
|
||||
const formatTypeList = (types: string[]): string => {
|
||||
if (types.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (types.length === 1) {
|
||||
return types[0];
|
||||
}
|
||||
|
||||
if (types.length === 2) {
|
||||
return `${types[0]} or ${types[1]}`;
|
||||
}
|
||||
|
||||
return `${types.slice(0, -1).join(', ')}, or ${types.at(-1)}`;
|
||||
};
|
||||
|
||||
export const buildUploadHint = ({
|
||||
allowedMimesCsv,
|
||||
maxFileBytes,
|
||||
maxFiles,
|
||||
maxFilesLabel,
|
||||
encryptedAtRest
|
||||
}: BuildUploadHintOptions): string => {
|
||||
const types = [
|
||||
...new Set(
|
||||
allowedMimesCsv
|
||||
.split(',')
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.map(m => MIME_LABEL[m] ?? m)
|
||||
)
|
||||
];
|
||||
|
||||
const sizePart = formatFileSize(maxFileBytes);
|
||||
const typePart = formatTypeList(types) || allowedMimesCsv.trim();
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (typePart) {
|
||||
parts.push(`${typePart} up to ${sizePart} each.`);
|
||||
} else {
|
||||
parts.push(`Up to ${sizePart} each.`);
|
||||
}
|
||||
|
||||
if (maxFiles !== undefined && maxFilesLabel) {
|
||||
parts.push(`Max ${maxFiles} ${maxFilesLabel}.`);
|
||||
}
|
||||
|
||||
if (encryptedAtRest) {
|
||||
parts.push('Encrypted at rest on the server.');
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
import { config } from '@/config';
|
||||
|
||||
export const resolveUploadPublicUrl = (publicPath: string): string => `${config.api.rootUrl}${publicPath}`;
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { UploadValidationOptions } from '@/types/UploadValidationOptions';
|
||||
import { formatFileSize } from '@/utils/formatFileSize';
|
||||
|
||||
export const validateUpload = (file: File, opts: UploadValidationOptions): string | null => {
|
||||
const { allowedMimesCsv, maxFileBytes } = opts;
|
||||
|
||||
const allowed = allowedMimesCsv
|
||||
.split(',')
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
if (allowed.length > 0) {
|
||||
const type = file.type.toLowerCase();
|
||||
|
||||
if (!type || !allowed.includes(type)) {
|
||||
return 'That file type is not allowed.';
|
||||
}
|
||||
}
|
||||
|
||||
if (file.size > maxFileBytes) {
|
||||
return `File is too large (max ${formatFileSize(maxFileBytes)}).`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
Reference in New Issue
Block a user