feat: all project setup
This commit is contained in:
102
lib/utils/constants.ts
Normal file
102
lib/utils/constants.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const API_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || "https://api.tgex.app/v1";
|
||||
export const USE_MOCKS = process.env.NEXT_PUBLIC_USE_MOCKS === "true";
|
||||
export const BOT_USERNAME = process.env.NEXT_PUBLIC_BOT_USERNAME || "tgex_bot";
|
||||
|
||||
// Local Storage Keys
|
||||
export const STORAGE_KEYS = {
|
||||
ACCESS_TOKEN: "tgex_access_token",
|
||||
REFRESH_TOKEN: "tgex_refresh_token",
|
||||
USER: "tgex_user",
|
||||
} as const;
|
||||
|
||||
// API Endpoints
|
||||
export const API_ENDPOINTS = {
|
||||
// Auth
|
||||
AUTH_INIT: "/auth/init",
|
||||
AUTH_COMPLETE: "/auth/complete",
|
||||
AUTH_REFRESH: "/auth/refresh",
|
||||
AUTH_ME: "/auth/me",
|
||||
|
||||
// Target Channels
|
||||
TARGET_CHANNELS: "/target-channels",
|
||||
TARGET_CHANNEL: (id: string) => `/target-channels/${id}`,
|
||||
|
||||
// External Channels
|
||||
EXTERNAL_CHANNELS: "/external-channels",
|
||||
EXTERNAL_CHANNEL: (id: string) => `/external-channels/${id}`,
|
||||
EXTERNAL_CHANNELS_IMPORT: "/external-channels/import",
|
||||
|
||||
// Creatives
|
||||
CREATIVES: "/creatives",
|
||||
CREATIVE: (id: string) => `/creatives/${id}`,
|
||||
|
||||
// Purchases
|
||||
PURCHASES: "/purchases",
|
||||
PURCHASE: (id: string) => `/purchases/${id}`,
|
||||
PURCHASE_REFRESH_VIEWS: (id: string) => `/purchases/${id}/refresh-views`,
|
||||
|
||||
// Analytics
|
||||
ANALYTICS_OVERVIEW: "/analytics/overview",
|
||||
ANALYTICS_COSTS: "/analytics/costs",
|
||||
ANALYTICS_CHANNELS: "/analytics/channels",
|
||||
ANALYTICS_CREATIVES: "/analytics/creatives",
|
||||
} as const;
|
||||
|
||||
// Error Codes
|
||||
export const ERROR_CODES = {
|
||||
UNAUTHORIZED: "UNAUTHORIZED",
|
||||
FORBIDDEN: "FORBIDDEN",
|
||||
NOT_FOUND: "NOT_FOUND",
|
||||
VALIDATION_ERROR: "VALIDATION_ERROR",
|
||||
INTERNAL_ERROR: "INTERNAL_ERROR",
|
||||
CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
|
||||
CHANNEL_ALREADY_EXISTS: "CHANNEL_ALREADY_EXISTS",
|
||||
INSUFFICIENT_PERMISSIONS: "INSUFFICIENT_PERMISSIONS",
|
||||
CREATIVE_IN_USE: "CREATIVE_IN_USE",
|
||||
INVALID_INVITE_LINK: "INVALID_INVITE_LINK",
|
||||
} as const;
|
||||
|
||||
// Date Formats
|
||||
export const DATE_FORMATS = {
|
||||
DISPLAY: "dd.MM.yyyy",
|
||||
DISPLAY_WITH_TIME: "dd.MM.yyyy HH:mm",
|
||||
API: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||
} as const;
|
||||
|
||||
// Pagination
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
|
||||
// Routes
|
||||
export const ROUTES = {
|
||||
HOME: "/",
|
||||
LOGIN: "/login",
|
||||
AUTH_COMPLETE: "/auth-complete",
|
||||
|
||||
CHANNELS: "/channels",
|
||||
CHANNEL_DETAIL: (id: string) => `/channels/${id}`,
|
||||
|
||||
EXTERNAL_CHANNELS: "/external-channels",
|
||||
EXTERNAL_CHANNEL_DETAIL: (id: string) => `/external-channels/${id}`,
|
||||
EXTERNAL_CHANNELS_IMPORT: "/external-channels/import",
|
||||
|
||||
CREATIVES: "/creatives",
|
||||
CREATIVE_NEW: "/creatives/new",
|
||||
CREATIVE_DETAIL: (id: string) => `/creatives/${id}`,
|
||||
CREATIVE_EDIT: (id: string) => `/creatives/${id}/edit`,
|
||||
|
||||
PURCHASES: "/purchases",
|
||||
PURCHASE_NEW: "/purchases/new",
|
||||
PURCHASE_DETAIL: (id: string) => `/purchases/${id}`,
|
||||
PURCHASE_EDIT: (id: string) => `/purchases/${id}/edit`,
|
||||
|
||||
ANALYTICS: "/analytics",
|
||||
ANALYTICS_COSTS: "/analytics/costs",
|
||||
ANALYTICS_CHANNELS: "/analytics/channels",
|
||||
ANALYTICS_CREATIVES: "/analytics/creatives",
|
||||
} as const;
|
||||
164
lib/utils/format.ts
Normal file
164
lib/utils/format.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
// ============================================================================
|
||||
// Formatting Utilities
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Форматирование числа с разделением тысяч
|
||||
*/
|
||||
export const formatNumber = (
|
||||
value: number | null | undefined,
|
||||
showSign: boolean = false
|
||||
): string => {
|
||||
if (value === null || value === undefined) return "—";
|
||||
const sign = showSign && value > 0 ? "+" : "";
|
||||
return `${sign}${new Intl.NumberFormat("ru-RU").format(value)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование валюты (рубли)
|
||||
*/
|
||||
export const formatCurrency = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return "—";
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
style: "currency",
|
||||
currency: "RUB",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование CPF/CPM с двумя знаками после запятой
|
||||
*/
|
||||
export const formatMetric = (value: number | null | undefined): string => {
|
||||
if (value === null || value === undefined) return "—";
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование процента
|
||||
*/
|
||||
export const formatPercent = (
|
||||
value: number | null | undefined,
|
||||
showSign: boolean = false
|
||||
): string => {
|
||||
if (value === null || value === undefined) return "—";
|
||||
const sign = showSign && value > 0 ? "+" : "";
|
||||
return `${sign}${formatMetric(value)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование даты
|
||||
*/
|
||||
export const formatDate = (
|
||||
date: string | Date | null | undefined,
|
||||
includeTime: boolean = false
|
||||
): string => {
|
||||
if (!date) return "—";
|
||||
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
|
||||
if (includeTime) {
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(d);
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat("ru-RU", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
}).format(d);
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование относительной даты (сколько прошло)
|
||||
*/
|
||||
export const formatRelativeDate = (date: string | Date): string => {
|
||||
const d = typeof date === "string" ? new Date(date) : date;
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - d.getTime();
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 7) {
|
||||
return formatDate(d);
|
||||
} else if (days > 0) {
|
||||
return `${days} ${pluralize(days, "день", "дня", "дней")} назад`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours} ${pluralize(hours, "час", "часа", "часов")} назад`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes} ${pluralize(
|
||||
minutes,
|
||||
"минута",
|
||||
"минуты",
|
||||
"минут"
|
||||
)} назад`;
|
||||
} else {
|
||||
return "только что";
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Плюрализация русских слов
|
||||
*/
|
||||
export const pluralize = (
|
||||
count: number,
|
||||
one: string,
|
||||
few: string,
|
||||
many: string
|
||||
): string => {
|
||||
const mod10 = count % 10;
|
||||
const mod100 = count % 100;
|
||||
|
||||
if (mod10 === 1 && mod100 !== 11) {
|
||||
return one;
|
||||
} else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
|
||||
return few;
|
||||
} else {
|
||||
return many;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Сокращение больших чисел (1000 -> 1K)
|
||||
*/
|
||||
export const formatCompactNumber = (
|
||||
value: number | null | undefined
|
||||
): string => {
|
||||
if (value === null || value === undefined) return "—";
|
||||
|
||||
if (value >= 1000000) {
|
||||
return `${(value / 1000000).toFixed(1)}M`;
|
||||
} else if (value >= 1000) {
|
||||
return `${(value / 1000).toFixed(1)}K`;
|
||||
}
|
||||
|
||||
return formatNumber(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Форматирование username (с @)
|
||||
*/
|
||||
export const formatUsername = (username: string | null | undefined): string => {
|
||||
if (!username) return "—";
|
||||
return username.startsWith("@") ? username : `@${username}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Обрезка длинного текста
|
||||
*/
|
||||
export const truncate = (text: string, maxLength: number = 50): string => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.substring(0, maxLength)}...`;
|
||||
};
|
||||
Reference in New Issue
Block a user