feat: all project setup

This commit is contained in:
ivannoskov
2025-11-10 12:07:24 +03:00
parent fbfd7719bb
commit b0a9934220
99 changed files with 19597 additions and 0 deletions

102
lib/utils/constants.ts Normal file
View 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
View 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)}...`;
};