feat: global platform v2 update
This commit is contained in:
@@ -4,41 +4,49 @@
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
PlacementAnalyticsItem,
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
SpendingAnalytics,
|
||||
PlacementsAnalytics,
|
||||
CreativesAnalytics,
|
||||
ExternalChannelsAnalytics,
|
||||
DateGrouping,
|
||||
AnalyticsQueryParams,
|
||||
SpendingAnalyticsQueryParams,
|
||||
PaginatedResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const analyticsApi = {
|
||||
/**
|
||||
* Get spending analytics
|
||||
*/
|
||||
spending: (params?: {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
grouping?: DateGrouping;
|
||||
}) => api.get<SpendingAnalytics>("/analytics/spending", { params }),
|
||||
|
||||
/**
|
||||
* Get placements analytics
|
||||
*/
|
||||
placements: (params?: { target_channel_id?: string }) =>
|
||||
api.get<PlacementsAnalytics>("/analytics/placements", { params }),
|
||||
placements: (workspaceId: string, params?: AnalyticsQueryParams) =>
|
||||
api.get<PaginatedResponse<PlacementAnalyticsItem>>(
|
||||
`/workspaces/${workspaceId}/analytics/placements`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Get creatives analytics
|
||||
*/
|
||||
creatives: (params?: { target_channel_id?: string }) =>
|
||||
api.get<CreativesAnalytics>("/analytics/creatives", { params }),
|
||||
creatives: (workspaceId: string, params?: AnalyticsQueryParams) =>
|
||||
api.get<PaginatedResponse<CreativeAnalyticsItem>>(
|
||||
`/workspaces/${workspaceId}/analytics/creatives`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Get external channels analytics
|
||||
* Get channels analytics
|
||||
*/
|
||||
externalChannels: (params?: { target_channel_id?: string }) =>
|
||||
api.get<ExternalChannelsAnalytics>("/analytics/external-channels", {
|
||||
params,
|
||||
}),
|
||||
channels: (workspaceId: string, params?: AnalyticsQueryParams) =>
|
||||
api.get<PaginatedResponse<ChannelAnalyticsItem>>(
|
||||
`/workspaces/${workspaceId}/analytics/channels`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Get spending analytics (time-series data)
|
||||
*/
|
||||
spending: (workspaceId: string, params?: SpendingAnalyticsQueryParams) =>
|
||||
api.get<SpendingAnalytics>(
|
||||
`/workspaces/${workspaceId}/analytics/spending`,
|
||||
{ params }
|
||||
),
|
||||
};
|
||||
|
||||
@@ -2,33 +2,19 @@
|
||||
// Auth API
|
||||
// ============================================================================
|
||||
|
||||
import { api, saveAuthTokens, clearAuthTokens } from "./client";
|
||||
import { api, clearAuthTokens } from "./client";
|
||||
import { STORAGE_KEYS } from "@/lib/utils/constants";
|
||||
import type {
|
||||
User,
|
||||
AuthInitResponse,
|
||||
AuthCompleteRequest,
|
||||
AuthCompleteResponse,
|
||||
AuthRefreshRequest,
|
||||
AuthRefreshResponse,
|
||||
} from "@/lib/types/api";
|
||||
import type { User, AuthCompleteResponse } from "@/lib/types/api";
|
||||
|
||||
export const authApi = {
|
||||
/**
|
||||
* Initialize Telegram auth flow
|
||||
*/
|
||||
init: () => api.get<AuthInitResponse>("/auth/init", { requireAuth: false }),
|
||||
|
||||
/**
|
||||
* Complete auth with one-time token from bot
|
||||
* GET /auth/complete?token=...
|
||||
*/
|
||||
complete: async (token: string): Promise<{ access_token: string }> => {
|
||||
// GET request with token as query parameter
|
||||
const response = await api.get<{ access_token: string }>(
|
||||
complete: async (token: string): Promise<AuthCompleteResponse> => {
|
||||
const response = await api.get<AuthCompleteResponse>(
|
||||
`/auth/complete?token=${token}`,
|
||||
{
|
||||
requireAuth: false,
|
||||
}
|
||||
{ requireAuth: false }
|
||||
);
|
||||
|
||||
// Save access token to storage
|
||||
@@ -39,36 +25,9 @@ export const authApi = {
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
refresh: async (): Promise<string> => {
|
||||
if (typeof window === "undefined") {
|
||||
throw new Error("Cannot refresh token on server");
|
||||
}
|
||||
|
||||
const refreshToken = localStorage.getItem(STORAGE_KEYS.REFRESH_TOKEN);
|
||||
if (!refreshToken) {
|
||||
throw new Error("No refresh token available");
|
||||
}
|
||||
|
||||
const data: AuthRefreshRequest = { refresh_token: refreshToken };
|
||||
const response = await api.post<AuthRefreshResponse>(
|
||||
"/auth/refresh",
|
||||
data,
|
||||
{
|
||||
requireAuth: false,
|
||||
}
|
||||
);
|
||||
|
||||
// Update access token
|
||||
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, response.access_token);
|
||||
|
||||
return response.access_token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user info
|
||||
* GET /auth/me
|
||||
*/
|
||||
me: () => api.get<User>("/auth/me"),
|
||||
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
// ============================================================================
|
||||
// Target Channels API
|
||||
// Channels API (Catalog for Placements)
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
TargetChannel,
|
||||
TargetChannelsListResponse,
|
||||
SuccessResponse,
|
||||
Channel,
|
||||
ChannelsQueryParams,
|
||||
PaginatedResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const channelsApi = {
|
||||
/**
|
||||
* Get list of target channels
|
||||
* GET /api/v1/target_channels
|
||||
* Get catalog of channels for placements
|
||||
*/
|
||||
list: () => api.get<TargetChannelsListResponse>("/target_channels"),
|
||||
|
||||
/**
|
||||
* Delete (disconnect) target channel
|
||||
* DELETE /api/v1/target_channels/{channel_id}
|
||||
*/
|
||||
delete: (id: string) => api.delete<SuccessResponse>(`/target_channels/${id}`),
|
||||
list: (params?: ChannelsQueryParams) =>
|
||||
api.get<PaginatedResponse<Channel>>("/channels", { params }),
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
// ============================================================================
|
||||
// API Client with Mock Support
|
||||
// API Client
|
||||
// ============================================================================
|
||||
|
||||
import { API_URL, USE_MOCKS, STORAGE_KEYS } from "@/lib/utils/constants";
|
||||
import { mockApiHandlers } from "@/lib/mocks/handlers";
|
||||
import { API_URL, STORAGE_KEYS } from "@/lib/utils/constants";
|
||||
import type { ApiError } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
@@ -61,169 +60,13 @@ export const clearAuthTokens = (): void => {
|
||||
*/
|
||||
export const saveAuthTokens = (
|
||||
accessToken: string,
|
||||
refreshToken: string
|
||||
refreshToken?: string
|
||||
): void => {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
||||
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Mock Router
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Route request to appropriate mock handler
|
||||
*/
|
||||
const routeMockRequest = async (
|
||||
endpoint: string,
|
||||
options: RequestOptions
|
||||
): Promise<any> => {
|
||||
const method = options.method || "GET";
|
||||
const params = options.params || {};
|
||||
const body = options.body ? JSON.parse(options.body as string) : undefined;
|
||||
|
||||
console.log(`[MOCK API] ${method} ${endpoint}`, { params, body });
|
||||
|
||||
// Auth endpoints
|
||||
if (endpoint === "/auth/init") {
|
||||
return mockApiHandlers.authInit();
|
||||
if (refreshToken) {
|
||||
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
||||
}
|
||||
if (endpoint.startsWith("/auth/complete")) {
|
||||
// Extract token from query params
|
||||
const token =
|
||||
params.token ||
|
||||
(endpoint.includes("?")
|
||||
? new URLSearchParams(endpoint.split("?")[1]).get("token")
|
||||
: null);
|
||||
return mockApiHandlers.authComplete({ token });
|
||||
}
|
||||
if (endpoint === "/auth/refresh") {
|
||||
return mockApiHandlers.authRefresh(body);
|
||||
}
|
||||
if (endpoint === "/auth/me") {
|
||||
return mockApiHandlers.authMe();
|
||||
}
|
||||
|
||||
// Target Channels
|
||||
if (endpoint === "/target_channels" && method === "GET") {
|
||||
return mockApiHandlers.getTargetChannels();
|
||||
}
|
||||
if (endpoint.startsWith("/target_channels/") && method === "DELETE") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.deleteTargetChannel(id);
|
||||
}
|
||||
|
||||
// External Channels
|
||||
if (endpoint.startsWith("/external_channels/target/") && method === "GET") {
|
||||
const targetChannelId = endpoint.split("/")[3];
|
||||
return mockApiHandlers.getExternalChannels(targetChannelId);
|
||||
}
|
||||
if (endpoint === "/external_channels" && method === "POST") {
|
||||
return mockApiHandlers.createExternalChannel(body);
|
||||
}
|
||||
if (
|
||||
endpoint.startsWith("/external_channels/") &&
|
||||
!endpoint.includes("/target/") &&
|
||||
!endpoint.includes("/links") &&
|
||||
method === "GET"
|
||||
) {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.getExternalChannel(id);
|
||||
}
|
||||
if (
|
||||
endpoint.startsWith("/external_channels/") &&
|
||||
endpoint.includes("/links") &&
|
||||
method === "PATCH"
|
||||
) {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.updateExternalChannelLinks(id, body);
|
||||
}
|
||||
if (
|
||||
endpoint.startsWith("/external_channels/") &&
|
||||
!endpoint.includes("/links") &&
|
||||
method === "PATCH"
|
||||
) {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.updateExternalChannel(id, body);
|
||||
}
|
||||
if (endpoint.startsWith("/external_channels/") && method === "DELETE") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.deleteExternalChannel(id);
|
||||
}
|
||||
|
||||
// Creatives
|
||||
if (endpoint === "/creatives" && method === "GET") {
|
||||
return mockApiHandlers.getCreatives(params);
|
||||
}
|
||||
if (endpoint === "/creatives" && method === "POST") {
|
||||
return mockApiHandlers.createCreative(body);
|
||||
}
|
||||
if (endpoint.startsWith("/creatives/") && method === "GET") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.getCreative(id);
|
||||
}
|
||||
if (endpoint.startsWith("/creatives/") && method === "PATCH") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.updateCreative(id, body);
|
||||
}
|
||||
if (endpoint.startsWith("/creatives/") && method === "DELETE") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.deleteCreative(id);
|
||||
}
|
||||
|
||||
// Placements
|
||||
if (endpoint === "/placements" && method === "GET") {
|
||||
return mockApiHandlers.getPlacements(params);
|
||||
}
|
||||
if (endpoint === "/placements" && method === "POST") {
|
||||
return mockApiHandlers.createPlacement(body);
|
||||
}
|
||||
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "GET") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.getPlacement(id);
|
||||
}
|
||||
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "PATCH") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.updatePlacement(id, body);
|
||||
}
|
||||
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "DELETE") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.deletePlacement(id);
|
||||
}
|
||||
if (endpoint.includes("/views/fetch") && method === "POST") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.fetchPlacementViews(id);
|
||||
}
|
||||
if (endpoint.includes("/views/history") && method === "GET") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.getPlacementViewsHistory(id, params);
|
||||
}
|
||||
if (endpoint.includes("/views/manual") && method === "POST") {
|
||||
const id = endpoint.split("/")[2];
|
||||
return mockApiHandlers.setPlacementViewsManually(id, params?.views_count);
|
||||
}
|
||||
|
||||
// Analytics
|
||||
if (endpoint === "/analytics/spending") {
|
||||
return mockApiHandlers.getSpendingAnalytics(params);
|
||||
}
|
||||
if (endpoint === "/analytics/placements") {
|
||||
return mockApiHandlers.getPlacementsAnalytics(params);
|
||||
}
|
||||
if (endpoint === "/analytics/creatives") {
|
||||
return mockApiHandlers.getCreativesAnalytics(params);
|
||||
}
|
||||
if (endpoint === "/analytics/external-channels") {
|
||||
return mockApiHandlers.getExternalChannelsAnalytics(params);
|
||||
}
|
||||
|
||||
throw {
|
||||
error: {
|
||||
code: "NOT_IMPLEMENTED",
|
||||
message: `Mock handler not implemented for ${method} ${endpoint}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@@ -231,7 +74,7 @@ const routeMockRequest = async (
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Main API request function with mock support
|
||||
* Main API request function
|
||||
*/
|
||||
export const apiRequest = async <T = any>(
|
||||
endpoint: string,
|
||||
@@ -239,17 +82,7 @@ export const apiRequest = async <T = any>(
|
||||
): Promise<T> => {
|
||||
const { params, requireAuth = true, ...fetchOptions } = options;
|
||||
|
||||
// Use mocks if enabled
|
||||
if (USE_MOCKS) {
|
||||
try {
|
||||
const result = await routeMockRequest(endpoint, options);
|
||||
return result as T;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Real API request
|
||||
// Build URL
|
||||
const url = buildUrl(`${API_URL}${endpoint}`, params);
|
||||
|
||||
const headers: HeadersInit = {
|
||||
@@ -271,45 +104,29 @@ export const apiRequest = async <T = any>(
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle 204 No Content
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const data = await response.json();
|
||||
|
||||
// Handle errors
|
||||
if (!response.ok) {
|
||||
// FastAPI returns errors in format { "detail": "..." }
|
||||
// Transform to our ApiError format
|
||||
if (data.detail) {
|
||||
throw {
|
||||
error: {
|
||||
code:
|
||||
response.status === 401
|
||||
? "UNAUTHORIZED"
|
||||
: response.status === 403
|
||||
? "FORBIDDEN"
|
||||
: response.status === 404
|
||||
? "NOT_FOUND"
|
||||
: response.status === 422
|
||||
? "VALIDATION_ERROR"
|
||||
: "API_ERROR",
|
||||
message: data.detail,
|
||||
},
|
||||
detail: data.detail,
|
||||
};
|
||||
}
|
||||
// If it's already in our format, throw as is
|
||||
const error: ApiError = data;
|
||||
const error: ApiError = {
|
||||
detail: data.detail || "An error occurred",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data as T;
|
||||
} catch (error: any) {
|
||||
// Network error or parsing error
|
||||
if (!error.error) {
|
||||
if (!error.detail) {
|
||||
throw {
|
||||
error: {
|
||||
code: "NETWORK_ERROR",
|
||||
message: error.message || "Network error occurred",
|
||||
},
|
||||
detail: error.message || "Network error occurred",
|
||||
} as ApiError;
|
||||
}
|
||||
throw error;
|
||||
@@ -328,47 +145,23 @@ export const api = {
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
put: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: "PUT",
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
patch: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
||||
apiRequest<T>(endpoint, {
|
||||
...options,
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(body),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
}),
|
||||
|
||||
delete: <T = any>(endpoint: string, options?: RequestOptions) =>
|
||||
apiRequest<T>(endpoint, { ...options, method: "DELETE" }),
|
||||
|
||||
// Special method for file upload
|
||||
upload: async <T = any>(
|
||||
endpoint: string,
|
||||
formData: FormData,
|
||||
options?: RequestOptions
|
||||
): Promise<T> => {
|
||||
// File import is now done on client side, no need for mock handling
|
||||
const url = `${API_URL}${endpoint}`;
|
||||
const token = getAuthToken();
|
||||
|
||||
const headers: HeadersInit = {};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData,
|
||||
...options,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw data as ApiError;
|
||||
}
|
||||
|
||||
return data as T;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,39 +5,57 @@
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
Creative,
|
||||
CreativeQueryParams,
|
||||
CreativeCreateRequest,
|
||||
CreativeUpdateRequest,
|
||||
CreativesListResponse,
|
||||
SuccessResponse,
|
||||
CreativesQueryParams,
|
||||
PaginatedResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const creativesApi = {
|
||||
/**
|
||||
* Get list of creatives
|
||||
* Get list of creatives for workspace
|
||||
*/
|
||||
list: (params?: CreativeQueryParams) =>
|
||||
api.get<CreativesListResponse>("/creatives", { params }),
|
||||
list: (workspaceId: string, params?: CreativesQueryParams) =>
|
||||
api.get<PaginatedResponse<Creative>>(
|
||||
`/workspaces/${workspaceId}/creatives`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Get creative details
|
||||
* Get single creative
|
||||
*/
|
||||
get: (id: string) => api.get<Creative>(`/creatives/${id}`),
|
||||
get: (workspaceId: string, creativeId: string) =>
|
||||
api.get<Creative>(`/workspaces/${workspaceId}/creatives/${creativeId}`),
|
||||
|
||||
/**
|
||||
* Create new creative
|
||||
* @param projectId - Required query param for the project
|
||||
*/
|
||||
create: (data: CreativeCreateRequest) =>
|
||||
api.post<Creative>("/creatives", data),
|
||||
create: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
data: CreativeCreateRequest
|
||||
) =>
|
||||
api.post<Creative>(`/workspaces/${workspaceId}/creatives`, data, {
|
||||
params: { project_id: projectId },
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update creative
|
||||
*/
|
||||
update: (id: string, data: CreativeUpdateRequest) =>
|
||||
api.patch<Creative>(`/creatives/${id}`, data),
|
||||
update: (
|
||||
workspaceId: string,
|
||||
creativeId: string,
|
||||
data: CreativeUpdateRequest
|
||||
) =>
|
||||
api.patch<Creative>(
|
||||
`/workspaces/${workspaceId}/creatives/${creativeId}`,
|
||||
data
|
||||
),
|
||||
|
||||
/**
|
||||
* Delete creative
|
||||
* Delete (archive) creative
|
||||
*/
|
||||
delete: (id: string) => api.delete<SuccessResponse>(`/creatives/${id}`),
|
||||
delete: (workspaceId: string, creativeId: string) =>
|
||||
api.delete<void>(`/workspaces/${workspaceId}/creatives/${creativeId}`),
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
// ============================================================================
|
||||
// External Channels API
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
ExternalChannel,
|
||||
ExternalChannelsListResponse,
|
||||
ExternalChannelCreateRequest,
|
||||
ExternalChannelUpdateRequest,
|
||||
ExternalChannelLinksUpdateRequest,
|
||||
SuccessResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const externalChannelsApi = {
|
||||
/**
|
||||
* Get list of external channels for target channel
|
||||
* GET /api/v1/external_channels/target/{target_channel_id}
|
||||
*/
|
||||
list: (targetChannelId: string) =>
|
||||
api.get<ExternalChannelsListResponse>(
|
||||
`/external_channels/target/${targetChannelId}`
|
||||
),
|
||||
|
||||
/**
|
||||
* Create new external channel
|
||||
* POST /api/v1/external_channels
|
||||
*/
|
||||
create: (data: ExternalChannelCreateRequest) =>
|
||||
api.post<ExternalChannel>("/external_channels", data),
|
||||
|
||||
/**
|
||||
* Update external channel
|
||||
* PATCH /api/v1/external_channels/{channel_id}
|
||||
*/
|
||||
update: (id: string, data: ExternalChannelUpdateRequest) =>
|
||||
api.patch<ExternalChannel>(`/external_channels/${id}`, data),
|
||||
|
||||
/**
|
||||
* Update external channel links
|
||||
* PATCH /api/v1/external_channels/{channel_id}/links
|
||||
*/
|
||||
updateLinks: (id: string, data: ExternalChannelLinksUpdateRequest) =>
|
||||
api.patch<ExternalChannel>(`/external_channels/${id}/links`, data),
|
||||
|
||||
/**
|
||||
* Delete external channel
|
||||
* DELETE /api/v1/external_channels/{channel_id}
|
||||
*/
|
||||
delete: (id: string) =>
|
||||
api.delete<SuccessResponse>(`/external_channels/${id}`),
|
||||
};
|
||||
@@ -1,11 +1,15 @@
|
||||
// ============================================================================
|
||||
// API Export
|
||||
// API Exports
|
||||
// ============================================================================
|
||||
|
||||
export * from "./client";
|
||||
export * from "./auth";
|
||||
export * from "./channels";
|
||||
export * from "./external-channels";
|
||||
export * from "./creatives";
|
||||
export * from "./placements";
|
||||
export * from "./analytics";
|
||||
export { api, apiRequest, clearAuthTokens, saveAuthTokens } from "./client";
|
||||
export { authApi } from "./auth";
|
||||
export { workspacesApi } from "./workspaces";
|
||||
export { membersApi } from "./members";
|
||||
export { invitesApi } from "./invites";
|
||||
export { projectsApi } from "./projects";
|
||||
export { channelsApi } from "./channels";
|
||||
export { purchasePlanApi } from "./purchase-plan";
|
||||
export { creativesApi } from "./creatives";
|
||||
export { placementsApi } from "./placements";
|
||||
export { analyticsApi } from "./analytics";
|
||||
|
||||
29
lib/api/invites.ts
Normal file
29
lib/api/invites.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// ============================================================================
|
||||
// Workspace Invites API
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
WorkspaceInvite,
|
||||
WorkspaceInviteCreateRequest,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const invitesApi = {
|
||||
/**
|
||||
* Get list of workspace invites
|
||||
*/
|
||||
list: (workspaceId: string, params?: PaginationParams) =>
|
||||
api.get<PaginatedResponse<WorkspaceInvite>>(
|
||||
`/workspaces/${workspaceId}/invites`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Create new invite
|
||||
*/
|
||||
create: (workspaceId: string, data: WorkspaceInviteCreateRequest) =>
|
||||
api.post<WorkspaceInvite>(`/workspaces/${workspaceId}/invites`, data),
|
||||
};
|
||||
|
||||
36
lib/api/members.ts
Normal file
36
lib/api/members.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// ============================================================================
|
||||
// Workspace Members API
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
WorkspaceMember,
|
||||
UpdatePermissionsRequest,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const membersApi = {
|
||||
/**
|
||||
* Get list of workspace members
|
||||
*/
|
||||
list: (workspaceId: string, params?: PaginationParams) =>
|
||||
api.get<PaginatedResponse<WorkspaceMember>>(
|
||||
`/workspaces/${workspaceId}/members`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Update member permissions
|
||||
*/
|
||||
updatePermissions: (
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
data: UpdatePermissionsRequest
|
||||
) =>
|
||||
api.put<WorkspaceMember>(
|
||||
`/workspaces/${workspaceId}/members/${userId}/permissions`,
|
||||
data
|
||||
),
|
||||
};
|
||||
|
||||
@@ -5,69 +5,65 @@
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
Placement,
|
||||
PlacementQueryParams,
|
||||
PlacementCreateRequest,
|
||||
PlacementUpdateRequest,
|
||||
PlacementsListResponse,
|
||||
ViewsFetchResponse,
|
||||
ViewsHistoryResponse,
|
||||
SuccessResponse,
|
||||
PlacementsQueryParams,
|
||||
ViewsHistoryItem,
|
||||
ViewsHistoryQueryParams,
|
||||
PaginatedResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export const placementsApi = {
|
||||
/**
|
||||
* Get list of placements
|
||||
* Get list of placements for workspace
|
||||
*/
|
||||
list: (params?: PlacementQueryParams) =>
|
||||
api.get<PlacementsListResponse>("/placements", { params }),
|
||||
list: (workspaceId: string, params?: PlacementsQueryParams) =>
|
||||
api.get<PaginatedResponse<Placement>>(
|
||||
`/workspaces/${workspaceId}/placements`,
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Get placement details
|
||||
* Get single placement
|
||||
*/
|
||||
get: (id: string) => api.get<Placement>(`/placements/${id}`),
|
||||
get: (workspaceId: string, placementId: string) =>
|
||||
api.get<Placement>(`/workspaces/${workspaceId}/placements/${placementId}`),
|
||||
|
||||
/**
|
||||
* Create new placement
|
||||
*/
|
||||
create: (data: PlacementCreateRequest) =>
|
||||
api.post<Placement>("/placements", data),
|
||||
create: (workspaceId: string, data: PlacementCreateRequest) =>
|
||||
api.post<Placement>(`/workspaces/${workspaceId}/placements`, data),
|
||||
|
||||
/**
|
||||
* Update placement
|
||||
*/
|
||||
update: (id: string, data: PlacementUpdateRequest) =>
|
||||
api.patch<Placement>(`/placements/${id}`, data),
|
||||
update: (
|
||||
workspaceId: string,
|
||||
placementId: string,
|
||||
data: PlacementUpdateRequest
|
||||
) =>
|
||||
api.patch<Placement>(
|
||||
`/workspaces/${workspaceId}/placements/${placementId}`,
|
||||
data
|
||||
),
|
||||
|
||||
/**
|
||||
* Delete placement
|
||||
* Delete (archive) placement
|
||||
*/
|
||||
delete: (id: string) => api.delete<SuccessResponse>(`/placements/${id}`),
|
||||
|
||||
/**
|
||||
* Fetch views for placement
|
||||
*/
|
||||
fetchViews: (id: string) =>
|
||||
api.post<ViewsFetchResponse>(`/placements/${id}/views/fetch`),
|
||||
delete: (workspaceId: string, placementId: string) =>
|
||||
api.delete<void>(`/workspaces/${workspaceId}/placements/${placementId}`),
|
||||
|
||||
/**
|
||||
* Get views history for placement
|
||||
*/
|
||||
viewsHistory: (
|
||||
id: string,
|
||||
params?: { from_date?: string; to_date?: string }
|
||||
getViewsHistory: (
|
||||
workspaceId: string,
|
||||
placementId: string,
|
||||
params?: ViewsHistoryQueryParams
|
||||
) =>
|
||||
api.get<ViewsHistoryResponse>(`/placements/${id}/views/history`, {
|
||||
params,
|
||||
}),
|
||||
|
||||
/**
|
||||
* Set views manually
|
||||
*/
|
||||
setViewsManually: (id: string, views_count: number) =>
|
||||
api.post<Placement>(`/placements/${id}/views/manual`, null, {
|
||||
params: { views_count },
|
||||
}),
|
||||
api.get<PaginatedResponse<ViewsHistoryItem>>(
|
||||
`/workspaces/${workspaceId}/placements/${placementId}/views/history`,
|
||||
{ params }
|
||||
),
|
||||
};
|
||||
|
||||
// Keep purchasesApi as an alias for backwards compatibility
|
||||
export const purchasesApi = placementsApi;
|
||||
|
||||
19
lib/api/projects.ts
Normal file
19
lib/api/projects.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// ============================================================================
|
||||
// Projects API (Target Channels)
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type { Project, PaginatedResponse, PaginationParams } from "@/lib/types/api";
|
||||
|
||||
export const projectsApi = {
|
||||
/**
|
||||
* Get list of projects for workspace
|
||||
* Projects are created via Telegram bot (add/remove channel)
|
||||
*/
|
||||
list: (workspaceId: string, params?: PaginationParams) =>
|
||||
api.get<PaginatedResponse<Project>>(
|
||||
`/workspaces/${workspaceId}/projects`,
|
||||
{ params }
|
||||
),
|
||||
};
|
||||
|
||||
61
lib/api/purchase-plan.ts
Normal file
61
lib/api/purchase-plan.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// ============================================================================
|
||||
// Purchase Plan API
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
PurchasePlanChannel,
|
||||
PurchasePlanChannelCreateRequest,
|
||||
PurchasePlanChannelUpdateRequest,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
const buildPath = (workspaceId: string, projectId: string) =>
|
||||
`/workspaces/${workspaceId}/projects/${projectId}/purchase-plan/channels`;
|
||||
|
||||
export const purchasePlanApi = {
|
||||
/**
|
||||
* Get list of channels in purchase plan
|
||||
*/
|
||||
list: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
params?: PaginationParams
|
||||
) =>
|
||||
api.get<PaginatedResponse<PurchasePlanChannel>>(
|
||||
buildPath(workspaceId, projectId),
|
||||
{ params }
|
||||
),
|
||||
|
||||
/**
|
||||
* Add channel to purchase plan
|
||||
*/
|
||||
create: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
data: PurchasePlanChannelCreateRequest
|
||||
) =>
|
||||
api.post<PurchasePlanChannel>(buildPath(workspaceId, projectId), data),
|
||||
|
||||
/**
|
||||
* Update channel in purchase plan
|
||||
*/
|
||||
update: (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
channelId: string,
|
||||
data: PurchasePlanChannelUpdateRequest
|
||||
) =>
|
||||
api.patch<PurchasePlanChannel>(
|
||||
`${buildPath(workspaceId, projectId)}/${channelId}`,
|
||||
data
|
||||
),
|
||||
|
||||
/**
|
||||
* Remove channel from purchase plan
|
||||
*/
|
||||
delete: (workspaceId: string, projectId: string, channelId: string) =>
|
||||
api.delete<void>(`${buildPath(workspaceId, projectId)}/${channelId}`),
|
||||
};
|
||||
|
||||
41
lib/api/workspaces.ts
Normal file
41
lib/api/workspaces.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// ============================================================================
|
||||
// Workspaces API
|
||||
// ============================================================================
|
||||
|
||||
import { api } from "./client";
|
||||
import type {
|
||||
Workspace,
|
||||
WorkspaceCreateRequest,
|
||||
WorkspaceUpdateRequest,
|
||||
PaginatedResponse,
|
||||
PaginationParams,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
const BASE_PATH = "/workspaces";
|
||||
|
||||
export const workspacesApi = {
|
||||
/**
|
||||
* Get list of workspaces for current user
|
||||
*/
|
||||
list: (params?: PaginationParams) =>
|
||||
api.get<PaginatedResponse<Workspace>>(BASE_PATH, { params }),
|
||||
|
||||
/**
|
||||
* Create new workspace
|
||||
*/
|
||||
create: (data: WorkspaceCreateRequest) =>
|
||||
api.post<Workspace>(BASE_PATH, data),
|
||||
|
||||
/**
|
||||
* Update workspace
|
||||
*/
|
||||
update: (workspaceId: string, data: WorkspaceUpdateRequest) =>
|
||||
api.patch<Workspace>(`${BASE_PATH}/${workspaceId}`, data),
|
||||
|
||||
/**
|
||||
* Delete workspace
|
||||
*/
|
||||
delete: (workspaceId: string) =>
|
||||
api.delete<void>(`${BASE_PATH}/${workspaceId}`),
|
||||
};
|
||||
|
||||
196
lib/demo/api.ts
Normal file
196
lib/demo/api.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
// ============================================================================
|
||||
// Demo API - Mock responses for demo mode
|
||||
// ============================================================================
|
||||
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
Workspace,
|
||||
Project,
|
||||
Channel,
|
||||
Creative,
|
||||
Placement,
|
||||
PurchasePlanChannel,
|
||||
WorkspaceMember,
|
||||
SpendingAnalytics,
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
} from "@/lib/types/api";
|
||||
import {
|
||||
demoWorkspace,
|
||||
demoMember,
|
||||
demoProjects,
|
||||
demoChannels,
|
||||
demoCreatives,
|
||||
demoPlacements,
|
||||
demoPurchasePlanChannels,
|
||||
demoSpendingAnalytics,
|
||||
demoCreativeAnalytics,
|
||||
demoChannelAnalytics,
|
||||
} from "./mock-data";
|
||||
import { DEMO_USER } from "./constants";
|
||||
|
||||
// ============================================================================
|
||||
// Helper to create paginated response
|
||||
// ============================================================================
|
||||
|
||||
const paginate = <T>(items: T[], page = 1, size = 50): PaginatedResponse<T> => ({
|
||||
items: items.slice((page - 1) * size, page * size),
|
||||
total: items.length,
|
||||
page,
|
||||
size,
|
||||
pages: Math.ceil(items.length / size),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Demo API Methods
|
||||
// ============================================================================
|
||||
|
||||
export const demoApi = {
|
||||
// Auth
|
||||
auth: {
|
||||
me: () => Promise.resolve(DEMO_USER),
|
||||
},
|
||||
|
||||
// Workspaces
|
||||
workspaces: {
|
||||
list: () => Promise.resolve(paginate([demoWorkspace])),
|
||||
get: () => Promise.resolve(demoWorkspace),
|
||||
},
|
||||
|
||||
// Members
|
||||
members: {
|
||||
list: () => Promise.resolve(paginate([demoMember])),
|
||||
},
|
||||
|
||||
// Projects
|
||||
projects: {
|
||||
list: (params?: { project_id?: string }) => {
|
||||
let items = demoProjects;
|
||||
if (params?.project_id) {
|
||||
items = items.filter((p) => p.id === params.project_id);
|
||||
}
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
},
|
||||
|
||||
// Channels
|
||||
channels: {
|
||||
list: () => Promise.resolve(paginate(demoChannels)),
|
||||
},
|
||||
|
||||
// Creatives
|
||||
creatives: {
|
||||
list: (params?: { project_id?: string; include_archived?: boolean }) => {
|
||||
let items = demoCreatives;
|
||||
if (params?.project_id) {
|
||||
items = items.filter((c) => c.project_id === params.project_id);
|
||||
}
|
||||
if (!params?.include_archived) {
|
||||
items = items.filter((c) => c.status === "active");
|
||||
}
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
get: (id: string) => {
|
||||
const creative = demoCreatives.find((c) => c.id === id);
|
||||
if (!creative) throw new Error("Creative not found");
|
||||
return Promise.resolve(creative);
|
||||
},
|
||||
},
|
||||
|
||||
// Placements
|
||||
placements: {
|
||||
list: (params?: {
|
||||
project_id?: string;
|
||||
placement_channel_id?: string;
|
||||
creative_id?: string;
|
||||
include_archived?: boolean;
|
||||
}) => {
|
||||
let items = demoPlacements;
|
||||
if (params?.project_id) {
|
||||
items = items.filter((p) => p.project_id === params.project_id);
|
||||
}
|
||||
if (params?.placement_channel_id) {
|
||||
items = items.filter(
|
||||
(p) => p.placement_channel_id === params.placement_channel_id
|
||||
);
|
||||
}
|
||||
if (params?.creative_id) {
|
||||
items = items.filter((p) => p.creative_id === params.creative_id);
|
||||
}
|
||||
if (!params?.include_archived) {
|
||||
items = items.filter((p) => p.status === "active");
|
||||
}
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
get: (id: string) => {
|
||||
const placement = demoPlacements.find((p) => p.id === id);
|
||||
if (!placement) throw new Error("Placement not found");
|
||||
return Promise.resolve(placement);
|
||||
},
|
||||
},
|
||||
|
||||
// Purchase Plan
|
||||
purchasePlan: {
|
||||
list: (projectId: string) => {
|
||||
const items = demoPurchasePlanChannels[projectId] || [];
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
},
|
||||
|
||||
// Analytics
|
||||
analytics: {
|
||||
spending: (params?: { project_id?: string; grouping?: string }) => {
|
||||
// Filter by project if specified
|
||||
if (params?.project_id) {
|
||||
const projectPlacements = demoPlacements.filter(
|
||||
(p) => p.project_id === params.project_id && p.status === "active"
|
||||
);
|
||||
const totalCost = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.cost || 0),
|
||||
0
|
||||
);
|
||||
const totalSubs = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.subscriptions_count || 0),
|
||||
0
|
||||
);
|
||||
const totalViews = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.views_count || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return Promise.resolve({
|
||||
total_cost: totalCost,
|
||||
total_subscriptions: totalSubs,
|
||||
total_views: totalViews,
|
||||
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
|
||||
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
|
||||
chart_data: demoSpendingAnalytics.chart_data.slice(0, 3),
|
||||
} as SpendingAnalytics);
|
||||
}
|
||||
return Promise.resolve(demoSpendingAnalytics);
|
||||
},
|
||||
creatives: (params?: { project_id?: string }) => {
|
||||
let items = demoCreativeAnalytics;
|
||||
if (params?.project_id) {
|
||||
const projectCreativeIds = demoCreatives
|
||||
.filter((c) => c.project_id === params.project_id)
|
||||
.map((c) => c.id);
|
||||
items = items.filter((a) => projectCreativeIds.includes(a.id));
|
||||
}
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
channels: (params?: { project_id?: string }) => {
|
||||
let items = demoChannelAnalytics;
|
||||
if (params?.project_id) {
|
||||
const projectPlacementChannelIds = new Set(
|
||||
demoPlacements
|
||||
.filter((p) => p.project_id === params.project_id)
|
||||
.map((p) => p.placement_channel_id)
|
||||
);
|
||||
items = items.filter((a) => projectPlacementChannelIds.has(a.channel_id));
|
||||
}
|
||||
return Promise.resolve(paginate(items));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
25
lib/demo/constants.ts
Normal file
25
lib/demo/constants.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// ============================================================================
|
||||
// Demo Mode Constants
|
||||
// ============================================================================
|
||||
|
||||
// Demo workspace ID - красивый и запоминающийся
|
||||
export const DEMO_WORKSPACE_ID = "demo";
|
||||
|
||||
// Check if workspace is demo
|
||||
export const isDemoWorkspace = (workspaceId: string | undefined): boolean => {
|
||||
return workspaceId === DEMO_WORKSPACE_ID;
|
||||
};
|
||||
|
||||
// Demo user
|
||||
export const DEMO_USER = {
|
||||
id: "demo-user-0000-0000-000000000000",
|
||||
telegram_id: 123456789,
|
||||
username: "demo_user",
|
||||
};
|
||||
|
||||
// Demo workspace
|
||||
export const DEMO_WORKSPACE = {
|
||||
id: DEMO_WORKSPACE_ID,
|
||||
name: "Демо Workspace",
|
||||
};
|
||||
|
||||
309
lib/demo/demo-aware-api.ts
Normal file
309
lib/demo/demo-aware-api.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
// ============================================================================
|
||||
// Demo-Aware API Wrapper
|
||||
// ============================================================================
|
||||
// This wrapper checks if the workspace is demo and returns mock data instead
|
||||
|
||||
import { isDemoWorkspace, DEMO_WORKSPACE_ID } from "./constants";
|
||||
import {
|
||||
demoChannels,
|
||||
demoCreatives,
|
||||
demoPlacements,
|
||||
demoPurchasePlanChannels,
|
||||
demoSpendingAnalytics,
|
||||
demoCreativeAnalytics,
|
||||
demoChannelAnalytics,
|
||||
demoProjects,
|
||||
demoWorkspace,
|
||||
demoMember,
|
||||
} from "./mock-data";
|
||||
import type {
|
||||
PaginatedResponse,
|
||||
Channel,
|
||||
Creative,
|
||||
Placement,
|
||||
PurchasePlanChannel,
|
||||
SpendingAnalytics,
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
Project,
|
||||
Workspace,
|
||||
WorkspaceMember,
|
||||
DateGrouping,
|
||||
} from "@/lib/types/api";
|
||||
import {
|
||||
workspacesApi,
|
||||
projectsApi,
|
||||
channelsApi,
|
||||
creativesApi,
|
||||
placementsApi,
|
||||
purchasePlanApi,
|
||||
analyticsApi,
|
||||
membersApi,
|
||||
} from "@/lib/api";
|
||||
|
||||
// ============================================================================
|
||||
// Helper
|
||||
// ============================================================================
|
||||
|
||||
const paginate = <T>(items: T[], page = 1, size = 50): PaginatedResponse<T> => ({
|
||||
items: items.slice((page - 1) * size, page * size),
|
||||
total: items.length,
|
||||
page,
|
||||
size,
|
||||
pages: Math.ceil(items.length / size),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Workspaces API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareWorkspacesApi = {
|
||||
list: async (params?: { page?: number; size?: number }) => {
|
||||
// Always include demo workspace in list for demo mode
|
||||
return workspacesApi.list(params);
|
||||
},
|
||||
get: async (workspaceId: string): Promise<Workspace> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
return demoWorkspace;
|
||||
}
|
||||
// Workspaces API doesn't have get method, fetch from list
|
||||
const response = await workspacesApi.list({ size: 100 });
|
||||
const workspace = response.items.find(w => w.id === workspaceId);
|
||||
if (!workspace) throw new Error("Workspace not found");
|
||||
return workspace;
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Projects API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareProjectsApi = {
|
||||
list: async (
|
||||
workspaceId: string,
|
||||
params?: { page?: number; size?: number }
|
||||
): Promise<PaginatedResponse<Project>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
return paginate(demoProjects, params?.page, params?.size);
|
||||
}
|
||||
return projectsApi.list(workspaceId, params);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Members API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareMembersApi = {
|
||||
list: async (
|
||||
workspaceId: string,
|
||||
params?: { page?: number; size?: number }
|
||||
): Promise<PaginatedResponse<WorkspaceMember>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
return paginate([demoMember], params?.page, params?.size);
|
||||
}
|
||||
return membersApi.list(workspaceId, params);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Channels API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareChannelsApi = {
|
||||
list: async (params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
username?: string;
|
||||
}): Promise<PaginatedResponse<Channel>> => {
|
||||
// Channels are global, check if we're in demo context via some mechanism
|
||||
// For now, always return real data - demo channels are handled per-workspace
|
||||
return channelsApi.list(params);
|
||||
},
|
||||
listForDemo: (): PaginatedResponse<Channel> => {
|
||||
return paginate(demoChannels);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Creatives API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareCreativesApi = {
|
||||
list: async (
|
||||
workspaceId: string,
|
||||
params?: {
|
||||
project_id?: string;
|
||||
include_archived?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
): Promise<PaginatedResponse<Creative>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
let items = demoCreatives;
|
||||
if (params?.project_id) {
|
||||
items = items.filter((c) => c.project_id === params.project_id);
|
||||
}
|
||||
if (!params?.include_archived) {
|
||||
items = items.filter((c) => c.status === "active");
|
||||
}
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return creativesApi.list(workspaceId, params);
|
||||
},
|
||||
get: async (workspaceId: string, creativeId: string): Promise<Creative> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
const creative = demoCreatives.find((c) => c.id === creativeId);
|
||||
if (!creative) throw new Error("Creative not found");
|
||||
return creative;
|
||||
}
|
||||
return creativesApi.get(workspaceId, creativeId);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Placements API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwarePlacementsApi = {
|
||||
list: async (
|
||||
workspaceId: string,
|
||||
params?: {
|
||||
project_id?: string;
|
||||
placement_channel_id?: string;
|
||||
creative_id?: string;
|
||||
include_archived?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
): Promise<PaginatedResponse<Placement>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
let items = demoPlacements;
|
||||
if (params?.project_id) {
|
||||
items = items.filter((p) => p.project_id === params.project_id);
|
||||
}
|
||||
if (params?.placement_channel_id) {
|
||||
items = items.filter(
|
||||
(p) => p.placement_channel_id === params.placement_channel_id
|
||||
);
|
||||
}
|
||||
if (params?.creative_id) {
|
||||
items = items.filter((p) => p.creative_id === params.creative_id);
|
||||
}
|
||||
if (!params?.include_archived) {
|
||||
items = items.filter((p) => p.status === "active");
|
||||
}
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return placementsApi.list(workspaceId, params);
|
||||
},
|
||||
get: async (workspaceId: string, placementId: string): Promise<Placement> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
const placement = demoPlacements.find((p) => p.id === placementId);
|
||||
if (!placement) throw new Error("Placement not found");
|
||||
return placement;
|
||||
}
|
||||
return placementsApi.get(workspaceId, placementId);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Purchase Plan API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwarePurchasePlanApi = {
|
||||
list: async (
|
||||
workspaceId: string,
|
||||
projectId: string,
|
||||
params?: { page?: number; size?: number }
|
||||
): Promise<PaginatedResponse<PurchasePlanChannel>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
const items = demoPurchasePlanChannels[projectId] || [];
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return purchasePlanApi.list(workspaceId, projectId, params);
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Demo-Aware Analytics API
|
||||
// ============================================================================
|
||||
|
||||
export const demoAwareAnalyticsApi = {
|
||||
spending: async (
|
||||
workspaceId: string,
|
||||
params?: {
|
||||
project_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
grouping?: DateGrouping;
|
||||
}
|
||||
): Promise<SpendingAnalytics> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
if (params?.project_id) {
|
||||
const projectPlacements = demoPlacements.filter(
|
||||
(p) => p.project_id === params.project_id && p.status === "active"
|
||||
);
|
||||
const totalCost = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.cost || 0),
|
||||
0
|
||||
);
|
||||
const totalSubs = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.subscriptions_count || 0),
|
||||
0
|
||||
);
|
||||
const totalViews = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.views_count || 0),
|
||||
0
|
||||
);
|
||||
return {
|
||||
total_cost: totalCost,
|
||||
total_subscriptions: totalSubs,
|
||||
total_views: totalViews,
|
||||
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
|
||||
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
|
||||
chart_data: demoSpendingAnalytics.chart_data.slice(0, 4),
|
||||
};
|
||||
}
|
||||
return demoSpendingAnalytics;
|
||||
}
|
||||
return analyticsApi.spending(workspaceId, params);
|
||||
},
|
||||
creatives: async (
|
||||
workspaceId: string,
|
||||
params?: { project_id?: string; page?: number; size?: number }
|
||||
): Promise<PaginatedResponse<CreativeAnalyticsItem>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
let items = demoCreativeAnalytics;
|
||||
if (params?.project_id) {
|
||||
const projectCreativeIds = demoCreatives
|
||||
.filter((c) => c.project_id === params.project_id)
|
||||
.map((c) => c.id);
|
||||
items = items.filter((a) => projectCreativeIds.includes(a.id));
|
||||
}
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return analyticsApi.creatives(workspaceId, params);
|
||||
},
|
||||
channels: async (
|
||||
workspaceId: string,
|
||||
params?: { project_id?: string; page?: number; size?: number }
|
||||
): Promise<PaginatedResponse<ChannelAnalyticsItem>> => {
|
||||
if (isDemoWorkspace(workspaceId)) {
|
||||
let items = demoChannelAnalytics;
|
||||
if (params?.project_id) {
|
||||
const projectPlacementChannelIds = new Set(
|
||||
demoPlacements
|
||||
.filter((p) => p.project_id === params.project_id)
|
||||
.map((p) => p.placement_channel_id)
|
||||
);
|
||||
items = items.filter((a) =>
|
||||
projectPlacementChannelIds.has(a.channel_id)
|
||||
);
|
||||
}
|
||||
return paginate(items, params?.page, params?.size);
|
||||
}
|
||||
return analyticsApi.channels(workspaceId, params);
|
||||
},
|
||||
};
|
||||
|
||||
272
lib/demo/hooks.ts
Normal file
272
lib/demo/hooks.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
// ============================================================================
|
||||
// Demo Mode Hooks
|
||||
// ============================================================================
|
||||
|
||||
import { useWorkspace } from "@/components/providers/workspace-provider";
|
||||
import { demoApi } from "./api";
|
||||
import {
|
||||
demoChannels,
|
||||
demoCreatives,
|
||||
demoPlacements,
|
||||
demoPurchasePlanChannels,
|
||||
demoSpendingAnalytics,
|
||||
demoCreativeAnalytics,
|
||||
demoChannelAnalytics,
|
||||
} from "./mock-data";
|
||||
import type {
|
||||
Channel,
|
||||
Creative,
|
||||
Placement,
|
||||
PurchasePlanChannel,
|
||||
SpendingAnalytics,
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
PaginatedResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
interface UseDemoDataResult<T> {
|
||||
data: T | null;
|
||||
isDemo: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Check if should use demo mode
|
||||
// ============================================================================
|
||||
|
||||
export const useIsDemoMode = (): boolean => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
return isDemoMode;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo channels
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoChannels = (): UseDemoDataResult<PaginatedResponse<Channel>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
return {
|
||||
data: {
|
||||
items: demoChannels,
|
||||
total: demoChannels.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo creatives
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoCreatives = (
|
||||
projectId?: string
|
||||
): UseDemoDataResult<PaginatedResponse<Creative>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
let items = demoCreatives;
|
||||
if (projectId) {
|
||||
items = items.filter((c) => c.project_id === projectId);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo placements
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoPlacements = (
|
||||
projectId?: string
|
||||
): UseDemoDataResult<PaginatedResponse<Placement>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
let items = demoPlacements;
|
||||
if (projectId) {
|
||||
items = items.filter((p) => p.project_id === projectId);
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo purchase plan
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoPurchasePlan = (
|
||||
projectId: string
|
||||
): UseDemoDataResult<PaginatedResponse<PurchasePlanChannel>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
const items = demoPurchasePlanChannels[projectId] || [];
|
||||
|
||||
return {
|
||||
data: {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo spending analytics
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoSpendingAnalytics = (
|
||||
projectId?: string
|
||||
): UseDemoDataResult<SpendingAnalytics> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
if (projectId) {
|
||||
// Filter by project
|
||||
const projectPlacements = demoPlacements.filter(
|
||||
(p) => p.project_id === projectId && p.status === "active"
|
||||
);
|
||||
const totalCost = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.cost || 0),
|
||||
0
|
||||
);
|
||||
const totalSubs = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.subscriptions_count || 0),
|
||||
0
|
||||
);
|
||||
const totalViews = projectPlacements.reduce(
|
||||
(sum, p) => sum + (p.views_count || 0),
|
||||
0
|
||||
);
|
||||
|
||||
return {
|
||||
data: {
|
||||
total_cost: totalCost,
|
||||
total_subscriptions: totalSubs,
|
||||
total_views: totalViews,
|
||||
avg_cpf: totalSubs > 0 ? totalCost / totalSubs : null,
|
||||
avg_cpm: totalViews > 0 ? (totalCost / totalViews) * 1000 : null,
|
||||
chart_data: demoSpendingAnalytics.chart_data.slice(0, 3),
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: demoSpendingAnalytics,
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo creative analytics
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoCreativeAnalytics = (
|
||||
projectId?: string
|
||||
): UseDemoDataResult<PaginatedResponse<CreativeAnalyticsItem>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
let items = demoCreativeAnalytics;
|
||||
if (projectId) {
|
||||
const projectCreativeIds = demoCreatives
|
||||
.filter((c) => c.project_id === projectId)
|
||||
.map((c) => c.id);
|
||||
items = items.filter((a) => projectCreativeIds.includes(a.id));
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Hook: Get demo channel analytics
|
||||
// ============================================================================
|
||||
|
||||
export const useDemoChannelAnalytics = (
|
||||
projectId?: string
|
||||
): UseDemoDataResult<PaginatedResponse<ChannelAnalyticsItem>> => {
|
||||
const { isDemoMode } = useWorkspace();
|
||||
|
||||
if (isDemoMode) {
|
||||
let items = demoChannelAnalytics;
|
||||
if (projectId) {
|
||||
const projectPlacementChannelIds = new Set(
|
||||
demoPlacements
|
||||
.filter((p) => p.project_id === projectId)
|
||||
.map((p) => p.placement_channel_id)
|
||||
);
|
||||
items = items.filter((a) => projectPlacementChannelIds.has(a.channel_id));
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
items,
|
||||
total: items.length,
|
||||
page: 1,
|
||||
size: 50,
|
||||
pages: 1,
|
||||
},
|
||||
isDemo: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { data: null, isDemo: false };
|
||||
};
|
||||
|
||||
10
lib/demo/index.ts
Normal file
10
lib/demo/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// ============================================================================
|
||||
// Demo Module Exports
|
||||
// ============================================================================
|
||||
|
||||
export * from "./constants";
|
||||
export * from "./mock-data";
|
||||
export { demoApi } from "./api";
|
||||
export * from "./hooks";
|
||||
export * from "./demo-aware-api";
|
||||
|
||||
583
lib/demo/mock-data.ts
Normal file
583
lib/demo/mock-data.ts
Normal file
@@ -0,0 +1,583 @@
|
||||
// ============================================================================
|
||||
// Demo Mock Data
|
||||
// ============================================================================
|
||||
|
||||
import type {
|
||||
Workspace,
|
||||
Project,
|
||||
Channel,
|
||||
Creative,
|
||||
Placement,
|
||||
PurchasePlanChannel,
|
||||
WorkspaceMember,
|
||||
Permission,
|
||||
SpendingAnalytics,
|
||||
CreativeAnalyticsItem,
|
||||
ChannelAnalyticsItem,
|
||||
} from "@/lib/types/api";
|
||||
import { DEMO_WORKSPACE_ID, DEMO_USER } from "./constants";
|
||||
|
||||
// ============================================================================
|
||||
// Workspace & User
|
||||
// ============================================================================
|
||||
|
||||
export const demoWorkspace: Workspace = {
|
||||
id: DEMO_WORKSPACE_ID,
|
||||
name: "Демо Workspace",
|
||||
};
|
||||
|
||||
export const demoMember: WorkspaceMember = {
|
||||
id: "demo-member-0000-0000-000000000001",
|
||||
user: {
|
||||
id: DEMO_USER.id,
|
||||
telegram_id: DEMO_USER.telegram_id,
|
||||
username: DEMO_USER.username,
|
||||
},
|
||||
status: "active",
|
||||
permissions: [{ key: "admin_full" }],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Projects (Target Channels)
|
||||
// ============================================================================
|
||||
|
||||
export const demoProjects: Project[] = [
|
||||
{
|
||||
id: "proj-0001-0000-0000-000000000001",
|
||||
telegram_id: -1001234567001,
|
||||
title: "Крипто Новости",
|
||||
username: "crypto_news_demo",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "proj-0002-0000-0000-000000000002",
|
||||
telegram_id: -1001234567002,
|
||||
title: "IT Вакансии",
|
||||
username: "it_jobs_demo",
|
||||
status: "active",
|
||||
},
|
||||
{
|
||||
id: "proj-0003-0000-0000-000000000003",
|
||||
telegram_id: -1001234567003,
|
||||
title: "Стартапы и Бизнес",
|
||||
username: "startups_demo",
|
||||
status: "active",
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Channels (External Channels for placements)
|
||||
// ============================================================================
|
||||
|
||||
export const demoChannels: Channel[] = [
|
||||
{
|
||||
id: "chan-0001-0000-0000-000000000001",
|
||||
telegram_id: -1002345678001,
|
||||
title: "Технологии Будущего",
|
||||
username: "tech_future",
|
||||
description: "Новости о технологиях и инновациях",
|
||||
subscribers_count: 125000,
|
||||
},
|
||||
{
|
||||
id: "chan-0002-0000-0000-000000000002",
|
||||
telegram_id: -1002345678002,
|
||||
title: "Инвестиции Pro",
|
||||
username: "invest_pro",
|
||||
description: "Аналитика и инвестиционные идеи",
|
||||
subscribers_count: 89000,
|
||||
},
|
||||
{
|
||||
id: "chan-0003-0000-0000-000000000003",
|
||||
telegram_id: -1002345678003,
|
||||
title: "Программисты",
|
||||
username: "programmers_hub",
|
||||
description: "Сообщество разработчиков",
|
||||
subscribers_count: 156000,
|
||||
},
|
||||
{
|
||||
id: "chan-0004-0000-0000-000000000004",
|
||||
telegram_id: -1002345678004,
|
||||
title: "Маркетинг Pro",
|
||||
username: "marketing_pro",
|
||||
description: "Digital маркетинг и реклама",
|
||||
subscribers_count: 67000,
|
||||
},
|
||||
{
|
||||
id: "chan-0005-0000-0000-000000000005",
|
||||
telegram_id: -1002345678005,
|
||||
title: "Финансовая грамотность",
|
||||
username: "finance_edu",
|
||||
description: "Обучение финансам",
|
||||
subscribers_count: 234000,
|
||||
},
|
||||
{
|
||||
id: "chan-0006-0000-0000-000000000006",
|
||||
telegram_id: -1002345678006,
|
||||
title: "Startup Daily",
|
||||
username: "startup_daily",
|
||||
description: "Ежедневные новости стартапов",
|
||||
subscribers_count: 45000,
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Creatives
|
||||
// ============================================================================
|
||||
|
||||
export const demoCreatives: Creative[] = [
|
||||
{
|
||||
id: "crea-0001-0000-0000-000000000001",
|
||||
name: "Крипто - Основной",
|
||||
text: "🚀 Присоединяйтесь к лучшему крипто-каналу!\n\n💰 Ежедневные сигналы\n📊 Аналитика рынка\n🎯 Точные прогнозы\n\n{invite_link}",
|
||||
status: "active",
|
||||
placements_count: 12,
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
created_at: "2024-11-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "crea-0002-0000-0000-000000000002",
|
||||
name: "Крипто - Агрессивный",
|
||||
text: "⚡️ СРОЧНО! Секретный канал для избранных!\n\n🔥 Закрытая информация\n💎 Альткоины x100\n🚀 Успей до памп!\n\n{invite_link}",
|
||||
status: "active",
|
||||
placements_count: 5,
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
created_at: "2024-11-20T14:30:00Z",
|
||||
},
|
||||
{
|
||||
id: "crea-0003-0000-0000-000000000003",
|
||||
name: "IT Jobs - Основной",
|
||||
text: "👨💻 Ищешь работу в IT?\n\n✅ Вакансии от топ-компаний\n✅ Удаленка и офис\n✅ Junior - Senior\n\nПодписывайся: {invite_link}",
|
||||
status: "active",
|
||||
placements_count: 8,
|
||||
project_id: "proj-0002-0000-0000-000000000002",
|
||||
project_channel_title: "IT Вакансии",
|
||||
created_at: "2024-10-01T09:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "crea-0004-0000-0000-000000000004",
|
||||
name: "Startups - Нетворкинг",
|
||||
text: "🤝 Нетворкинг для стартаперов\n\n🎯 Найди кофаундера\n💡 Обменяйся идеями\n🚀 Развивай проект\n\n{invite_link}",
|
||||
status: "active",
|
||||
placements_count: 6,
|
||||
project_id: "proj-0003-0000-0000-000000000003",
|
||||
project_channel_title: "Стартапы и Бизнес",
|
||||
created_at: "2024-12-01T16:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "crea-0005-0000-0000-000000000005",
|
||||
name: "IT Jobs - Тестовый",
|
||||
text: "🔥 Тестовый креатив для IT вакансий\n\n{invite_link}",
|
||||
status: "archived",
|
||||
placements_count: 2,
|
||||
project_id: "proj-0002-0000-0000-000000000002",
|
||||
project_channel_title: "IT Вакансии",
|
||||
created_at: "2024-09-15T11:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Placements
|
||||
// ============================================================================
|
||||
|
||||
export const demoPlacements: Placement[] = [
|
||||
{
|
||||
id: "plac-0001-0000-0000-000000000001",
|
||||
placement_date: "2024-12-20T12:00:00Z",
|
||||
cost: 5500,
|
||||
comment: "Отличное размещение",
|
||||
ad_post_url: "https://t.me/tech_future/1234",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+ABC123demo1",
|
||||
status: "active",
|
||||
subscriptions_count: 156,
|
||||
views_count: 8500,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-12-21T10:00:00Z",
|
||||
created_at: "2024-12-19T15:00:00Z",
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
placement_channel_id: "chan-0001-0000-0000-000000000001",
|
||||
placement_channel_title: "Технологии Будущего",
|
||||
creative_id: "crea-0001-0000-0000-000000000001",
|
||||
creative_name: "Крипто - Основной",
|
||||
},
|
||||
{
|
||||
id: "plac-0002-0000-0000-000000000002",
|
||||
placement_date: "2024-12-18T14:00:00Z",
|
||||
cost: 3200,
|
||||
comment: null,
|
||||
ad_post_url: "https://t.me/invest_pro/5678",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+ABC123demo2",
|
||||
status: "active",
|
||||
subscriptions_count: 89,
|
||||
views_count: 4200,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-12-20T08:00:00Z",
|
||||
created_at: "2024-12-17T10:00:00Z",
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
placement_channel_id: "chan-0002-0000-0000-000000000002",
|
||||
placement_channel_title: "Инвестиции Pro",
|
||||
creative_id: "crea-0001-0000-0000-000000000001",
|
||||
creative_name: "Крипто - Основной",
|
||||
},
|
||||
{
|
||||
id: "plac-0003-0000-0000-000000000003",
|
||||
placement_date: "2024-12-15T10:00:00Z",
|
||||
cost: 4800,
|
||||
comment: "Программисты - хорошая аудитория",
|
||||
ad_post_url: "https://t.me/programmers_hub/9012",
|
||||
invite_link_type: "public",
|
||||
invite_link: "https://t.me/+ABC123demo3",
|
||||
status: "active",
|
||||
subscriptions_count: 234,
|
||||
views_count: 12000,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-12-19T14:00:00Z",
|
||||
created_at: "2024-12-14T09:00:00Z",
|
||||
project_id: "proj-0002-0000-0000-000000000002",
|
||||
project_channel_title: "IT Вакансии",
|
||||
placement_channel_id: "chan-0003-0000-0000-000000000003",
|
||||
placement_channel_title: "Программисты",
|
||||
creative_id: "crea-0003-0000-0000-000000000003",
|
||||
creative_name: "IT Jobs - Основной",
|
||||
},
|
||||
{
|
||||
id: "plac-0004-0000-0000-000000000004",
|
||||
placement_date: "2024-12-10T16:00:00Z",
|
||||
cost: 2500,
|
||||
comment: null,
|
||||
ad_post_url: "https://t.me/marketing_pro/3456",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+ABC123demo4",
|
||||
status: "active",
|
||||
subscriptions_count: 67,
|
||||
views_count: 3100,
|
||||
views_availability: "manual",
|
||||
last_views_fetch_at: "2024-12-12T11:00:00Z",
|
||||
created_at: "2024-12-09T14:00:00Z",
|
||||
project_id: "proj-0003-0000-0000-000000000003",
|
||||
project_channel_title: "Стартапы и Бизнес",
|
||||
placement_channel_id: "chan-0004-0000-0000-000000000004",
|
||||
placement_channel_title: "Маркетинг Pro",
|
||||
creative_id: "crea-0004-0000-0000-000000000004",
|
||||
creative_name: "Startups - Нетворкинг",
|
||||
},
|
||||
{
|
||||
id: "plac-0005-0000-0000-000000000005",
|
||||
placement_date: "2024-12-05T11:00:00Z",
|
||||
cost: 6200,
|
||||
comment: "Финансовая аудитория",
|
||||
ad_post_url: "https://t.me/finance_edu/7890",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+ABC123demo5",
|
||||
status: "active",
|
||||
subscriptions_count: 312,
|
||||
views_count: 15600,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-12-08T09:00:00Z",
|
||||
created_at: "2024-12-04T10:00:00Z",
|
||||
project_id: "proj-0001-0000-0000-000000000001",
|
||||
project_channel_title: "Крипто Новости",
|
||||
placement_channel_id: "chan-0005-0000-0000-000000000005",
|
||||
placement_channel_title: "Финансовая грамотность",
|
||||
creative_id: "crea-0002-0000-0000-000000000002",
|
||||
creative_name: "Крипто - Агрессивный",
|
||||
},
|
||||
{
|
||||
id: "plac-0006-0000-0000-000000000006",
|
||||
placement_date: "2024-11-28T13:00:00Z",
|
||||
cost: 1800,
|
||||
comment: null,
|
||||
ad_post_url: "https://t.me/startup_daily/2345",
|
||||
invite_link_type: "public",
|
||||
invite_link: "https://t.me/+ABC123demo6",
|
||||
status: "archived",
|
||||
subscriptions_count: 45,
|
||||
views_count: 2100,
|
||||
views_availability: "unavailable",
|
||||
last_views_fetch_at: null,
|
||||
created_at: "2024-11-27T15:00:00Z",
|
||||
project_id: "proj-0003-0000-0000-000000000003",
|
||||
project_channel_title: "Стартапы и Бизнес",
|
||||
placement_channel_id: "chan-0006-0000-0000-000000000006",
|
||||
placement_channel_title: "Startup Daily",
|
||||
creative_id: "crea-0004-0000-0000-000000000004",
|
||||
creative_name: "Startups - Нетворкинг",
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Purchase Plan Channels
|
||||
// ============================================================================
|
||||
|
||||
export const demoPurchasePlanChannels: Record<string, PurchasePlanChannel[]> = {
|
||||
"proj-0001-0000-0000-000000000001": [
|
||||
{
|
||||
id: "plan-0001-0000-0000-000000000001",
|
||||
status: "completed",
|
||||
planned_cost: 5500,
|
||||
comment: "Основной канал",
|
||||
channel: {
|
||||
id: "chan-0001-0000-0000-000000000001",
|
||||
telegram_id: -1002345678001,
|
||||
title: "Технологии Будущего",
|
||||
username: "tech_future",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "plan-0002-0000-0000-000000000002",
|
||||
status: "completed",
|
||||
planned_cost: 3500,
|
||||
comment: null,
|
||||
channel: {
|
||||
id: "chan-0002-0000-0000-000000000002",
|
||||
telegram_id: -1002345678002,
|
||||
title: "Инвестиции Pro",
|
||||
username: "invest_pro",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "plan-0003-0000-0000-000000000003",
|
||||
status: "planned",
|
||||
planned_cost: 4000,
|
||||
comment: "На следующую неделю",
|
||||
channel: {
|
||||
id: "chan-0005-0000-0000-000000000005",
|
||||
telegram_id: -1002345678005,
|
||||
title: "Финансовая грамотность",
|
||||
username: "finance_edu",
|
||||
},
|
||||
},
|
||||
],
|
||||
"proj-0002-0000-0000-000000000002": [
|
||||
{
|
||||
id: "plan-0004-0000-0000-000000000004",
|
||||
status: "completed",
|
||||
planned_cost: 4800,
|
||||
comment: "IT аудитория",
|
||||
channel: {
|
||||
id: "chan-0003-0000-0000-000000000003",
|
||||
telegram_id: -1002345678003,
|
||||
title: "Программисты",
|
||||
username: "programmers_hub",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "plan-0005-0000-0000-000000000005",
|
||||
status: "planned",
|
||||
planned_cost: 3000,
|
||||
comment: null,
|
||||
channel: {
|
||||
id: "chan-0001-0000-0000-000000000001",
|
||||
telegram_id: -1002345678001,
|
||||
title: "Технологии Будущего",
|
||||
username: "tech_future",
|
||||
},
|
||||
},
|
||||
],
|
||||
"proj-0003-0000-0000-000000000003": [
|
||||
{
|
||||
id: "plan-0006-0000-0000-000000000006",
|
||||
status: "completed",
|
||||
planned_cost: 2500,
|
||||
comment: null,
|
||||
channel: {
|
||||
id: "chan-0004-0000-0000-000000000004",
|
||||
telegram_id: -1002345678004,
|
||||
title: "Маркетинг Pro",
|
||||
username: "marketing_pro",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "plan-0007-0000-0000-000000000007",
|
||||
status: "completed",
|
||||
planned_cost: 1800,
|
||||
comment: "Стартап аудитория",
|
||||
channel: {
|
||||
id: "chan-0006-0000-0000-000000000006",
|
||||
telegram_id: -1002345678006,
|
||||
title: "Startup Daily",
|
||||
username: "startup_daily",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Analytics Data
|
||||
// ============================================================================
|
||||
|
||||
export const demoSpendingAnalytics: SpendingAnalytics = {
|
||||
total_cost: 24000,
|
||||
total_subscriptions: 903,
|
||||
total_views: 45500,
|
||||
avg_cpf: 26.58,
|
||||
avg_cpm: 527.47,
|
||||
chart_data: [
|
||||
{
|
||||
period: "2024-11",
|
||||
cost: 1800,
|
||||
subscriptions: 45,
|
||||
views: 2100,
|
||||
cpf: 40.0,
|
||||
cpm: 857.14,
|
||||
},
|
||||
{
|
||||
period: "2024-12-05",
|
||||
cost: 6200,
|
||||
subscriptions: 312,
|
||||
views: 15600,
|
||||
cpf: 19.87,
|
||||
cpm: 397.44,
|
||||
},
|
||||
{
|
||||
period: "2024-12-10",
|
||||
cost: 2500,
|
||||
subscriptions: 67,
|
||||
views: 3100,
|
||||
cpf: 37.31,
|
||||
cpm: 806.45,
|
||||
},
|
||||
{
|
||||
period: "2024-12-15",
|
||||
cost: 4800,
|
||||
subscriptions: 234,
|
||||
views: 12000,
|
||||
cpf: 20.51,
|
||||
cpm: 400.0,
|
||||
},
|
||||
{
|
||||
period: "2024-12-18",
|
||||
cost: 3200,
|
||||
subscriptions: 89,
|
||||
views: 4200,
|
||||
cpf: 35.96,
|
||||
cpm: 761.9,
|
||||
},
|
||||
{
|
||||
period: "2024-12-20",
|
||||
cost: 5500,
|
||||
subscriptions: 156,
|
||||
views: 8500,
|
||||
cpf: 35.26,
|
||||
cpm: 647.06,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const demoCreativeAnalytics: CreativeAnalyticsItem[] = [
|
||||
{
|
||||
id: "crea-0001-0000-0000-000000000001",
|
||||
name: "Крипто - Основной",
|
||||
placements_count: 12,
|
||||
total_cost: 8700,
|
||||
total_subscriptions: 245,
|
||||
total_views: 12700,
|
||||
avg_cpf: 35.51,
|
||||
avg_cpm: 685.04,
|
||||
},
|
||||
{
|
||||
id: "crea-0002-0000-0000-000000000002",
|
||||
name: "Крипто - Агрессивный",
|
||||
placements_count: 5,
|
||||
total_cost: 6200,
|
||||
total_subscriptions: 312,
|
||||
total_views: 15600,
|
||||
avg_cpf: 19.87,
|
||||
avg_cpm: 397.44,
|
||||
},
|
||||
{
|
||||
id: "crea-0003-0000-0000-000000000003",
|
||||
name: "IT Jobs - Основной",
|
||||
placements_count: 8,
|
||||
total_cost: 4800,
|
||||
total_subscriptions: 234,
|
||||
total_views: 12000,
|
||||
avg_cpf: 20.51,
|
||||
avg_cpm: 400.0,
|
||||
},
|
||||
{
|
||||
id: "crea-0004-0000-0000-000000000004",
|
||||
name: "Startups - Нетворкинг",
|
||||
placements_count: 6,
|
||||
total_cost: 4300,
|
||||
total_subscriptions: 112,
|
||||
total_views: 5200,
|
||||
avg_cpf: 38.39,
|
||||
avg_cpm: 826.92,
|
||||
},
|
||||
];
|
||||
|
||||
export const demoChannelAnalytics: ChannelAnalyticsItem[] = [
|
||||
{
|
||||
channel_id: "chan-0001-0000-0000-000000000001",
|
||||
channel_title: "Технологии Будущего",
|
||||
channel_username: "tech_future",
|
||||
placements_count: 3,
|
||||
total_cost: 5500,
|
||||
total_subscriptions: 156,
|
||||
total_views: 8500,
|
||||
avg_cps: 35.26,
|
||||
avg_cpm: 647.06,
|
||||
},
|
||||
{
|
||||
channel_id: "chan-0002-0000-0000-000000000002",
|
||||
channel_title: "Инвестиции Pro",
|
||||
channel_username: "invest_pro",
|
||||
placements_count: 2,
|
||||
total_cost: 3200,
|
||||
total_subscriptions: 89,
|
||||
total_views: 4200,
|
||||
avg_cps: 35.96,
|
||||
avg_cpm: 761.9,
|
||||
},
|
||||
{
|
||||
channel_id: "chan-0003-0000-0000-000000000003",
|
||||
channel_title: "Программисты",
|
||||
channel_username: "programmers_hub",
|
||||
placements_count: 4,
|
||||
total_cost: 4800,
|
||||
total_subscriptions: 234,
|
||||
total_views: 12000,
|
||||
avg_cps: 20.51,
|
||||
avg_cpm: 400.0,
|
||||
},
|
||||
{
|
||||
channel_id: "chan-0005-0000-0000-000000000005",
|
||||
channel_title: "Финансовая грамотность",
|
||||
channel_username: "finance_edu",
|
||||
placements_count: 2,
|
||||
total_cost: 6200,
|
||||
total_subscriptions: 312,
|
||||
total_views: 15600,
|
||||
avg_cps: 19.87,
|
||||
avg_cpm: 397.44,
|
||||
},
|
||||
{
|
||||
channel_id: "chan-0004-0000-0000-000000000004",
|
||||
channel_title: "Маркетинг Pro",
|
||||
channel_username: "marketing_pro",
|
||||
placements_count: 1,
|
||||
total_cost: 2500,
|
||||
total_subscriptions: 67,
|
||||
total_views: 3100,
|
||||
avg_cps: 37.31,
|
||||
avg_cpm: 806.45,
|
||||
},
|
||||
{
|
||||
channel_id: "chan-0006-0000-0000-000000000006",
|
||||
channel_title: "Startup Daily",
|
||||
channel_username: "startup_daily",
|
||||
placements_count: 1,
|
||||
total_cost: 1800,
|
||||
total_subscriptions: 45,
|
||||
total_views: 2100,
|
||||
avg_cps: 40.0,
|
||||
avg_cpm: 857.14,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { TargetChannel } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Target Channels Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockTargetChannels: TargetChannel[] = [
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440010",
|
||||
telegram_id: -1001234567890,
|
||||
title: "Мой Главный Канал",
|
||||
username: "my_main_channel",
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440011",
|
||||
telegram_id: -1009876543210,
|
||||
title: "Тестовый Канал",
|
||||
username: "test_channel_promo",
|
||||
is_active: true,
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440012",
|
||||
telegram_id: -1005555555555,
|
||||
title: "Новости и Обновления",
|
||||
username: null,
|
||||
is_active: false,
|
||||
},
|
||||
];
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Creative } from "@/lib/types/api";
|
||||
import { mockTargetChannels } from "./channels";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Creatives Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockCreatives: Creative[] = [
|
||||
{
|
||||
id: "cr1",
|
||||
name: 'Креатив "Присоединяйся"',
|
||||
text: "🔥 Присоединяйся к нашему сообществу!\n\nУзнавай первым о новых возможностях и фишках.\n\n👉 {link}",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
created_at: "2024-01-15T10:00:00.000Z",
|
||||
status: "active",
|
||||
placements_count: 10,
|
||||
},
|
||||
{
|
||||
id: "cr2",
|
||||
name: 'Креатив "Эксклюзив"',
|
||||
text: "⭐ Эксклюзивный контент только для подписчиков!\n\nНе упусти возможность быть в курсе всех трендов.\n\nПереходи: {link}",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
created_at: "2024-01-20T12:00:00.000Z",
|
||||
status: "active",
|
||||
placements_count: 5,
|
||||
},
|
||||
{
|
||||
id: "cr3",
|
||||
name: 'Креатив "Обучение"',
|
||||
text: "📚 Хочешь научиться зарабатывать больше?\n\nПодписывайся на канал с проверенными методиками!\n\n{link}",
|
||||
target_channel_id: "tc2",
|
||||
target_channel_title: mockTargetChannels[1].title,
|
||||
created_at: "2024-02-05T09:00:00.000Z",
|
||||
status: "active",
|
||||
placements_count: 8,
|
||||
},
|
||||
{
|
||||
id: "cr4",
|
||||
name: "Старый креатив (архив)",
|
||||
text: "Устаревший текст с предложением. {link}",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
created_at: "2023-12-01T10:00:00.000Z",
|
||||
status: "archived",
|
||||
placements_count: 2,
|
||||
},
|
||||
];
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ExternalChannel } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Channels Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockExternalChannels: ExternalChannel[] = [
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440020",
|
||||
telegram_id: -1001111111111,
|
||||
title: "Канал о Маркетинге",
|
||||
username: "marketing_pro",
|
||||
subscribers_count: 50000,
|
||||
description: "Лучшие практики маркетинга",
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440021",
|
||||
telegram_id: -1002222222222,
|
||||
title: "Бизнес и Стартапы",
|
||||
username: "business_startups",
|
||||
subscribers_count: 120000,
|
||||
description: "Новости мира бизнеса",
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440022",
|
||||
telegram_id: -1003333333333,
|
||||
title: "IT и Технологии",
|
||||
username: "it_tech_news",
|
||||
subscribers_count: 85000,
|
||||
description: "Все о новых технологиях",
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440023",
|
||||
telegram_id: -1004444444444,
|
||||
title: "Крипто Инвестиции",
|
||||
username: "crypto_invest",
|
||||
subscribers_count: 200000,
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440024",
|
||||
telegram_id: -1005555555555,
|
||||
title: "Продажи и Переговоры",
|
||||
username: "sales_expert",
|
||||
subscribers_count: 35000,
|
||||
description: "Экспертиза в продажах",
|
||||
},
|
||||
];
|
||||
@@ -1,213 +0,0 @@
|
||||
import { Placement, Subscription, ViewsSnapshot } from "@/lib/types/api";
|
||||
import { mockTargetChannels } from "./channels";
|
||||
import { mockExternalChannels } from "./external-channels";
|
||||
import { mockCreatives } from "./creatives";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Placements Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockPlacements: Placement[] = [
|
||||
{
|
||||
id: "p1",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
external_channel_id: "ec1",
|
||||
external_channel_title: mockExternalChannels[0].title,
|
||||
creative_id: "cr1",
|
||||
creative_name: mockCreatives[0].name,
|
||||
placement_date: "2024-03-15T10:00:00.000Z",
|
||||
cost: 5000,
|
||||
comment: "Тестовая закупка на утро",
|
||||
ad_post_url: "https://t.me/marketing_pro/1234",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+AbCdEfGhIjKlMnOp",
|
||||
status: "active",
|
||||
subscriptions_count: 125,
|
||||
views_count: 12500,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-03-16T10:00:00.000Z",
|
||||
created_at: "2024-03-14T15:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "p2",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
external_channel_id: "ec2",
|
||||
external_channel_title: mockExternalChannels[1].title,
|
||||
creative_id: "cr2",
|
||||
creative_name: mockCreatives[1].name,
|
||||
placement_date: "2024-03-18T14:00:00.000Z",
|
||||
cost: 12000,
|
||||
comment: "Большой канал, ожидаем хороших результатов",
|
||||
ad_post_url: "https://t.me/business_startups/5678",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+QrStUvWxYzAbCdEf",
|
||||
status: "active",
|
||||
subscriptions_count: 210,
|
||||
views_count: 28000,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-03-19T10:00:00.000Z",
|
||||
created_at: "2024-03-17T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "p3",
|
||||
target_channel_id: "tc2",
|
||||
target_channel_title: mockTargetChannels[1].title,
|
||||
external_channel_id: "ec3",
|
||||
external_channel_title: mockExternalChannels[2].title,
|
||||
creative_id: "cr3",
|
||||
creative_name: mockCreatives[2].name,
|
||||
placement_date: "2024-03-20T16:00:00.000Z",
|
||||
cost: 3500,
|
||||
comment: null,
|
||||
ad_post_url: "https://t.me/it_tech_news/9101",
|
||||
invite_link_type: "public",
|
||||
invite_link: "https://t.me/+GhIjKlMnOpQrStUv",
|
||||
status: "active",
|
||||
subscriptions_count: 75,
|
||||
views_count: 8500,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-03-21T10:00:00.000Z",
|
||||
created_at: "2024-03-19T12:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "p4",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
external_channel_id: "ec1",
|
||||
external_channel_title: mockExternalChannels[0].title,
|
||||
creative_id: "cr1",
|
||||
creative_name: mockCreatives[0].name,
|
||||
placement_date: "2024-03-22T08:00:00.000Z",
|
||||
cost: 4500,
|
||||
comment: "Повтор в этом же канале",
|
||||
ad_post_url: "https://t.me/marketing_pro/1290",
|
||||
invite_link_type: "approval",
|
||||
invite_link: "https://t.me/+XyZaBcDeFgHiJkLm",
|
||||
status: "active",
|
||||
subscriptions_count: 95,
|
||||
views_count: null,
|
||||
views_availability: "unavailable",
|
||||
last_views_fetch_at: "2024-03-23T09:00:00.000Z",
|
||||
created_at: "2024-03-21T14:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "p5",
|
||||
target_channel_id: "tc1",
|
||||
target_channel_title: mockTargetChannels[0].title,
|
||||
external_channel_id: "ec2",
|
||||
external_channel_title: mockExternalChannels[1].title,
|
||||
creative_id: "cr1",
|
||||
creative_name: mockCreatives[0].name,
|
||||
placement_date: "2024-02-10T10:00:00.000Z",
|
||||
cost: 10000,
|
||||
comment: "Старая закупка (архив)",
|
||||
ad_post_url: "https://t.me/business_startups/4567",
|
||||
invite_link_type: "public",
|
||||
invite_link: "https://t.me/+OldLinkArch12345",
|
||||
status: "archived",
|
||||
subscriptions_count: 150,
|
||||
views_count: 20000,
|
||||
views_availability: "available",
|
||||
last_views_fetch_at: "2024-02-12T10:00:00.000Z",
|
||||
created_at: "2024-02-09T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
// Keep old name for backward compatibility
|
||||
export const mockPurchases = mockPlacements;
|
||||
|
||||
// ============================================================================
|
||||
// Mock Subscriptions
|
||||
// ============================================================================
|
||||
|
||||
export const mockSubscriptions: Subscription[] = [
|
||||
{
|
||||
id: "s1",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 123456789,
|
||||
user_first_name: "Иван",
|
||||
user_last_name: "Иванов",
|
||||
user_username: "ivan_ivanov",
|
||||
subscribed_at: "2024-03-15T11:35:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "join_request",
|
||||
},
|
||||
{
|
||||
id: "s2",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 987654321,
|
||||
user_first_name: "Мария",
|
||||
user_last_name: null,
|
||||
user_username: "maria123",
|
||||
subscribed_at: "2024-03-15T12:05:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "join_request",
|
||||
},
|
||||
{
|
||||
id: "s3",
|
||||
purchase_id: "p1",
|
||||
telegram_user_id: 456789123,
|
||||
user_first_name: "Алексей",
|
||||
user_last_name: "Петров",
|
||||
user_username: null,
|
||||
subscribed_at: "2024-03-15T13:20:00.000Z",
|
||||
is_active: false,
|
||||
event_type: "join_request",
|
||||
},
|
||||
{
|
||||
id: "s4",
|
||||
purchase_id: "p2",
|
||||
telegram_user_id: 789123456,
|
||||
user_first_name: "Елена",
|
||||
user_last_name: "Сидорова",
|
||||
user_username: "elena_s",
|
||||
subscribed_at: "2024-03-18T14:30:00.000Z",
|
||||
is_active: true,
|
||||
event_type: "chat_member",
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// Mock Views History
|
||||
// ============================================================================
|
||||
|
||||
export const mockViewsHistory: ViewsSnapshot[] = [
|
||||
{
|
||||
id: "v1",
|
||||
purchase_id: "p1",
|
||||
views: 10000,
|
||||
fetched_at: "2024-03-15T12:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v2",
|
||||
purchase_id: "p1",
|
||||
views: 11500,
|
||||
fetched_at: "2024-03-15T18:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v3",
|
||||
purchase_id: "p1",
|
||||
views: 12500,
|
||||
fetched_at: "2024-03-16T10:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v4",
|
||||
purchase_id: "p2",
|
||||
views: 25000,
|
||||
fetched_at: "2024-03-18T15:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v5",
|
||||
purchase_id: "p2",
|
||||
views: 27000,
|
||||
fetched_at: "2024-03-18T20:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "v6",
|
||||
purchase_id: "p2",
|
||||
views: 28000,
|
||||
fetched_at: "2024-03-19T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
import { User } from "@/lib/types/api";
|
||||
|
||||
// ============================================================================
|
||||
// Mock Users Data
|
||||
// ============================================================================
|
||||
|
||||
export const mockUsers: User[] = [
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440001",
|
||||
telegram_id: 123456789,
|
||||
username: "ivan_test",
|
||||
},
|
||||
{
|
||||
id: "550e8400-e29b-41d4-a716-446655440002",
|
||||
telegram_id: 987654321,
|
||||
username: "maria_test",
|
||||
},
|
||||
];
|
||||
|
||||
export const mockCurrentUser = mockUsers[0];
|
||||
|
||||
// Mock tokens
|
||||
export const mockTokens = {
|
||||
access_token: "mock_access_token_123456789",
|
||||
refresh_token: "mock_refresh_token_123456789",
|
||||
};
|
||||
@@ -1,573 +0,0 @@
|
||||
// ============================================================================
|
||||
// Mock API Handlers
|
||||
// ============================================================================
|
||||
|
||||
import {
|
||||
mockCurrentUser,
|
||||
mockTokens,
|
||||
mockTargetChannels,
|
||||
mockExternalChannels,
|
||||
mockCreatives,
|
||||
mockPlacements,
|
||||
mockSubscriptions,
|
||||
mockViewsHistory,
|
||||
} from "./index";
|
||||
|
||||
import type {
|
||||
ListResponse,
|
||||
SuccessResponse,
|
||||
ApiError,
|
||||
TargetChannel,
|
||||
ExternalChannel,
|
||||
Creative,
|
||||
Placement,
|
||||
PlacementCreateRequest,
|
||||
PlacementUpdateRequest,
|
||||
CreativeCreateRequest,
|
||||
ExternalChannelCreateRequest,
|
||||
ViewsFetchResponse,
|
||||
ViewsHistoryResponse,
|
||||
AuthCompleteRequest,
|
||||
AuthCompleteResponse,
|
||||
AuthRefreshRequest,
|
||||
AuthRefreshResponse,
|
||||
AuthInitResponse,
|
||||
ExternalChannelUpdateRequest,
|
||||
CreativeUpdateRequest,
|
||||
ExternalChannelsListResponse,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
// Simulate network delay
|
||||
const delay = (ms: number = 300) =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
// Helper to generate mock error
|
||||
const mockError = (code: string, message: string): ApiError => ({
|
||||
error: { code, message },
|
||||
});
|
||||
|
||||
// Helper to generate IDs
|
||||
let idCounter = 1000;
|
||||
const generateId = () => `mock_${idCounter++}`;
|
||||
|
||||
// ============================================================================
|
||||
// Mock API Handlers
|
||||
// ============================================================================
|
||||
|
||||
export const mockApiHandlers = {
|
||||
// --------------------------------------------------------------------------
|
||||
// Auth
|
||||
// --------------------------------------------------------------------------
|
||||
async authInit(): Promise<AuthInitResponse> {
|
||||
await delay();
|
||||
return {
|
||||
bot_username: "tgex_bot",
|
||||
start_param: "login",
|
||||
};
|
||||
},
|
||||
|
||||
async authComplete(
|
||||
data: AuthCompleteRequest | { token: string | null }
|
||||
): Promise<{ access_token: string }> {
|
||||
await delay();
|
||||
if (!data.token) {
|
||||
throw mockError("VALIDATION_ERROR", "Token is required");
|
||||
}
|
||||
|
||||
// Save mock user to localStorage for later retrieval
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("tgex_user", JSON.stringify(mockCurrentUser));
|
||||
}
|
||||
|
||||
return {
|
||||
access_token: mockTokens.access_token,
|
||||
};
|
||||
},
|
||||
|
||||
async authRefresh(data: AuthRefreshRequest): Promise<AuthRefreshResponse> {
|
||||
await delay();
|
||||
if (!data.refresh_token) {
|
||||
throw mockError("VALIDATION_ERROR", "Refresh token is required");
|
||||
}
|
||||
return {
|
||||
access_token: `${mockTokens.access_token}_refreshed`,
|
||||
};
|
||||
},
|
||||
|
||||
async authMe() {
|
||||
await delay();
|
||||
return mockCurrentUser;
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Target Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getTargetChannels() {
|
||||
await delay();
|
||||
return {
|
||||
target_channels: mockTargetChannels,
|
||||
};
|
||||
},
|
||||
|
||||
async deleteTargetChannel(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const channel = mockTargetChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// External Channels
|
||||
// --------------------------------------------------------------------------
|
||||
async getExternalChannels(
|
||||
targetChannelId: string
|
||||
): Promise<ExternalChannelsListResponse> {
|
||||
await delay();
|
||||
// Возвращаем все каналы для указанного target channel
|
||||
// В реальном API это будет фильтрация по связям через промежуточную таблицу
|
||||
return {
|
||||
external_channels: mockExternalChannels,
|
||||
};
|
||||
},
|
||||
|
||||
async getExternalChannel(id: string): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
return channel;
|
||||
},
|
||||
|
||||
async createExternalChannel(
|
||||
data: ExternalChannelCreateRequest
|
||||
): Promise<ExternalChannel> {
|
||||
await delay(500);
|
||||
const newChannel: ExternalChannel = {
|
||||
id: generateId(),
|
||||
telegram_id: data.telegram_id,
|
||||
title: data.title,
|
||||
username: data.username || null,
|
||||
subscribers_count: data.subscribers_count || null,
|
||||
description: data.description || null,
|
||||
};
|
||||
mockExternalChannels.push(newChannel);
|
||||
return newChannel;
|
||||
},
|
||||
|
||||
async updateExternalChannel(
|
||||
id: string,
|
||||
data: ExternalChannelUpdateRequest
|
||||
): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
return {
|
||||
...channel,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
async deleteExternalChannel(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const index = mockExternalChannels.findIndex((c) => c.id === id);
|
||||
if (index === -1) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
mockExternalChannels.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
async updateExternalChannelLinks(
|
||||
id: string,
|
||||
data: {
|
||||
add_target_channel_ids?: string[];
|
||||
remove_target_channel_ids?: string[];
|
||||
}
|
||||
): Promise<ExternalChannel> {
|
||||
await delay();
|
||||
const channel = mockExternalChannels.find((c) => c.id === id);
|
||||
if (!channel) {
|
||||
throw mockError("NOT_FOUND", "External channel not found");
|
||||
}
|
||||
// В упрощенной версии просто возвращаем канал
|
||||
return channel;
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Creatives
|
||||
// --------------------------------------------------------------------------
|
||||
async getCreatives(params?: {
|
||||
target_channel_id?: string;
|
||||
include_archived?: boolean;
|
||||
}) {
|
||||
await delay();
|
||||
let filtered = [...mockCreatives];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(c) => c.target_channel_id === params.target_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (!params?.include_archived) {
|
||||
filtered = filtered.filter((c) => c.status !== "archived");
|
||||
}
|
||||
|
||||
return {
|
||||
creatives: filtered,
|
||||
};
|
||||
},
|
||||
|
||||
async getCreative(id: string): Promise<Creative> {
|
||||
await delay();
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
return creative;
|
||||
},
|
||||
|
||||
async createCreative(data: CreativeCreateRequest): Promise<Creative> {
|
||||
await delay(500);
|
||||
|
||||
const targetChannel = mockTargetChannels.find(
|
||||
(c) => c.id === data.target_channel_id
|
||||
);
|
||||
if (!targetChannel) {
|
||||
throw mockError("NOT_FOUND", "Target channel not found");
|
||||
}
|
||||
|
||||
const newCreative: Creative = {
|
||||
id: generateId(),
|
||||
name: data.name,
|
||||
text: data.text,
|
||||
target_channel_id: data.target_channel_id,
|
||||
target_channel_title: targetChannel.title,
|
||||
created_at: new Date().toISOString(),
|
||||
status: "active",
|
||||
placements_count: 0,
|
||||
};
|
||||
mockCreatives.push(newCreative);
|
||||
return newCreative;
|
||||
},
|
||||
|
||||
async updateCreative(
|
||||
id: string,
|
||||
data: CreativeUpdateRequest
|
||||
): Promise<Creative> {
|
||||
await delay();
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
|
||||
if (data.text && !data.text.includes("{link}")) {
|
||||
throw mockError(
|
||||
"VALIDATION_ERROR",
|
||||
"Creative text must contain {link} placeholder"
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...creative,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
async deleteCreative(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const creative = mockCreatives.find((c) => c.id === id);
|
||||
if (!creative) {
|
||||
throw mockError("NOT_FOUND", "Creative not found");
|
||||
}
|
||||
|
||||
if (creative.placements_count > 0) {
|
||||
throw mockError(
|
||||
"CREATIVE_IN_USE",
|
||||
"Cannot delete creative with active placements"
|
||||
);
|
||||
}
|
||||
|
||||
const index = mockCreatives.findIndex((c) => c.id === id);
|
||||
mockCreatives.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Placements (Purchases)
|
||||
// --------------------------------------------------------------------------
|
||||
async getPlacements(params?: {
|
||||
target_channel_id?: string;
|
||||
external_channel_id?: string;
|
||||
creative_id?: string;
|
||||
include_archived?: boolean;
|
||||
}) {
|
||||
await delay();
|
||||
let filtered = [...mockPlacements];
|
||||
|
||||
if (params?.target_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(p) => p.target_channel_id === params.target_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.external_channel_id) {
|
||||
filtered = filtered.filter(
|
||||
(p) => p.external_channel_id === params.external_channel_id
|
||||
);
|
||||
}
|
||||
|
||||
if (params?.creative_id) {
|
||||
filtered = filtered.filter((p) => p.creative_id === params.creative_id);
|
||||
}
|
||||
|
||||
if (!params?.include_archived) {
|
||||
filtered = filtered.filter((p) => p.status !== "archived");
|
||||
}
|
||||
|
||||
return {
|
||||
placements: filtered,
|
||||
};
|
||||
},
|
||||
|
||||
async getPlacement(id: string): Promise<Placement> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
return placement;
|
||||
},
|
||||
|
||||
async createPlacement(data: PlacementCreateRequest): Promise<Placement> {
|
||||
await delay(800);
|
||||
|
||||
const targetChannel = mockTargetChannels.find(
|
||||
(c) => c.id === data.target_channel_id
|
||||
);
|
||||
const externalChannel = mockExternalChannels.find(
|
||||
(c) => c.id === data.external_channel_id
|
||||
);
|
||||
const creative = mockCreatives.find((c) => c.id === data.creative_id);
|
||||
|
||||
if (!targetChannel || !externalChannel || !creative) {
|
||||
throw mockError("NOT_FOUND", "One of the required entities not found");
|
||||
}
|
||||
|
||||
// Generate mock invite link
|
||||
const inviteLink = `https://t.me/+${Math.random()
|
||||
.toString(36)
|
||||
.substring(2, 15)}`;
|
||||
|
||||
const newPlacement: Placement = {
|
||||
id: generateId(),
|
||||
target_channel_id: data.target_channel_id,
|
||||
target_channel_title: targetChannel.title,
|
||||
external_channel_id: data.external_channel_id,
|
||||
external_channel_title: externalChannel.title,
|
||||
creative_id: data.creative_id,
|
||||
creative_name: creative.name,
|
||||
placement_date: data.placement_date,
|
||||
cost: data.cost || null,
|
||||
comment: data.comment || null,
|
||||
ad_post_url: data.ad_post_url || null,
|
||||
invite_link_type: data.invite_link_type || "public",
|
||||
invite_link: inviteLink,
|
||||
status: "active",
|
||||
subscriptions_count: 0,
|
||||
views_count: null,
|
||||
views_availability: "unknown",
|
||||
last_views_fetch_at: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
mockPlacements.push(newPlacement);
|
||||
return newPlacement;
|
||||
},
|
||||
|
||||
async updatePlacement(
|
||||
id: string,
|
||||
data: PlacementUpdateRequest
|
||||
): Promise<Placement> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...placement,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
async deletePlacement(id: string): Promise<SuccessResponse> {
|
||||
await delay();
|
||||
const index = mockPlacements.findIndex((p) => p.id === id);
|
||||
if (index === -1) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
mockPlacements.splice(index, 1);
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
async fetchPlacementViews(id: string): Promise<ViewsFetchResponse> {
|
||||
await delay(1000);
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
// Mock: add some random views
|
||||
const newViews =
|
||||
(placement.views_count || 0) + Math.floor(Math.random() * 500);
|
||||
|
||||
return {
|
||||
placement_id: id,
|
||||
views_count: newViews,
|
||||
views_availability: "available",
|
||||
fetched_at: new Date().toISOString(),
|
||||
error_message: null,
|
||||
};
|
||||
},
|
||||
|
||||
async getPlacementViewsHistory(
|
||||
id: string,
|
||||
params?: { from_date?: string; to_date?: string }
|
||||
): Promise<ViewsHistoryResponse> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
const filteredHistory = mockViewsHistory.filter(
|
||||
(v) => v.purchase_id === id
|
||||
);
|
||||
return {
|
||||
histories: filteredHistory,
|
||||
};
|
||||
},
|
||||
|
||||
async setPlacementViewsManually(
|
||||
id: string,
|
||||
views_count: number
|
||||
): Promise<Placement> {
|
||||
await delay();
|
||||
const placement = mockPlacements.find((p) => p.id === id);
|
||||
if (!placement) {
|
||||
throw mockError("NOT_FOUND", "Placement not found");
|
||||
}
|
||||
|
||||
return {
|
||||
...placement,
|
||||
views_count,
|
||||
views_availability: "manual",
|
||||
last_views_fetch_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Analytics
|
||||
// --------------------------------------------------------------------------
|
||||
async getSpendingAnalytics(params?: {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
grouping?: "day" | "week" | "month" | "quarter" | "year";
|
||||
}) {
|
||||
await delay();
|
||||
return {
|
||||
total_cost: 45000,
|
||||
total_subscriptions: 850,
|
||||
total_views: 125000,
|
||||
avg_cpf: 52.94,
|
||||
avg_cpm: 360,
|
||||
chart_data: [
|
||||
{
|
||||
period: "2024-03-01",
|
||||
cost: 15000,
|
||||
subscriptions: 280,
|
||||
views: 42000,
|
||||
cpf: 53.57,
|
||||
cpm: 357.14,
|
||||
},
|
||||
{
|
||||
period: "2024-03-08",
|
||||
cost: 18000,
|
||||
subscriptions: 340,
|
||||
views: 51000,
|
||||
cpf: 52.94,
|
||||
cpm: 352.94,
|
||||
},
|
||||
{
|
||||
period: "2024-03-15",
|
||||
cost: 12000,
|
||||
subscriptions: 230,
|
||||
views: 32000,
|
||||
cpf: 52.17,
|
||||
cpm: 375,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
async getPlacementsAnalytics(params?: { target_channel_id?: string }) {
|
||||
await delay();
|
||||
return {
|
||||
placements: mockPlacements.map((p) => ({
|
||||
id: p.id,
|
||||
target_channel_id: p.target_channel_id,
|
||||
target_channel_title: p.target_channel_title,
|
||||
external_channel_id: p.external_channel_id,
|
||||
external_channel_title: p.external_channel_title,
|
||||
creative_id: p.creative_id,
|
||||
creative_name: p.creative_name,
|
||||
placement_date: p.placement_date,
|
||||
cost: p.cost,
|
||||
subscriptions_count: p.subscriptions_count,
|
||||
views_count: p.views_count,
|
||||
cpf: p.cost && p.subscriptions_count > 0 ? p.cost / p.subscriptions_count : null,
|
||||
cpm: p.cost && p.views_count ? (p.cost / p.views_count) * 1000 : null,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async getCreativesAnalytics(params?: { target_channel_id?: string }) {
|
||||
await delay();
|
||||
return {
|
||||
creatives: mockCreatives.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
placements_count: c.placements_count,
|
||||
total_cost: 15000,
|
||||
total_subscriptions: 280,
|
||||
total_views: 42000,
|
||||
avg_cpf: 53.57,
|
||||
avg_cpm: 357.14,
|
||||
})),
|
||||
};
|
||||
},
|
||||
|
||||
async getExternalChannelsAnalytics(params?: { target_channel_id?: string }) {
|
||||
await delay();
|
||||
return {
|
||||
external_channels: mockExternalChannels.map((ec) => ({
|
||||
id: ec.id,
|
||||
title: ec.title,
|
||||
username: ec.username,
|
||||
placements_count: 3,
|
||||
total_cost: 12000,
|
||||
total_subscriptions: 210,
|
||||
total_views: 28000,
|
||||
avg_cpf: 57.14,
|
||||
avg_cpm: 428.57,
|
||||
})),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// ============================================================================
|
||||
// Mock Data Export
|
||||
// ============================================================================
|
||||
|
||||
export * from "./data/users";
|
||||
export * from "./data/channels";
|
||||
export * from "./data/external-channels";
|
||||
export * from "./data/creatives";
|
||||
export * from "./data/placements";
|
||||
484
lib/types/api.ts
484
lib/types/api.ts
@@ -1,5 +1,5 @@
|
||||
// ============================================================================
|
||||
// API Types - Generated from API_DOCUMENTATION.md
|
||||
// API Types - Based on actual API responses
|
||||
// ============================================================================
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -12,148 +12,220 @@ export interface User {
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface AuthInitResponse {
|
||||
bot_username: string;
|
||||
start_param: string; // "login"
|
||||
}
|
||||
|
||||
export interface AuthCompleteRequest {
|
||||
token: string; // one-time token from bot
|
||||
}
|
||||
|
||||
export interface AuthCompleteResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface AuthRefreshRequest {
|
||||
refresh_token: string;
|
||||
}
|
||||
|
||||
export interface AuthRefreshResponse {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Target Channel
|
||||
// Pagination
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Основная структура как возвращает API
|
||||
export interface TargetChannel {
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Workspaces
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface Workspace {
|
||||
id: string; // UUID
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceCreateRequest {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceUpdateRequest {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Workspace Members & Permissions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type WorkspaceUserStatus = "active" | "invited";
|
||||
|
||||
export type PermissionKey =
|
||||
| "admin_full"
|
||||
| "projects_read"
|
||||
| "projects_write"
|
||||
| "placements_read"
|
||||
| "placements_write"
|
||||
| "analytics_read";
|
||||
|
||||
export type PermissionScopeType = "project";
|
||||
|
||||
export interface PermissionScope {
|
||||
type: PermissionScopeType;
|
||||
id: string; // UUID
|
||||
}
|
||||
|
||||
export interface Permission {
|
||||
key: PermissionKey;
|
||||
scopes?: PermissionScope[];
|
||||
}
|
||||
|
||||
export interface WorkspaceMemberUser {
|
||||
id: string; // user_id
|
||||
telegram_id: number;
|
||||
username: string | null;
|
||||
}
|
||||
|
||||
export interface WorkspaceMember {
|
||||
id: string; // member record id
|
||||
status: WorkspaceUserStatus;
|
||||
user: WorkspaceMemberUser;
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
export interface UpdatePermissionsRequest {
|
||||
permissions: Permission[];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Workspace Invites
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type WorkspaceInviteStatus = "pending" | "accepted" | "rejected";
|
||||
|
||||
export interface WorkspaceInvite {
|
||||
id: string; // UUID
|
||||
username: string;
|
||||
status: WorkspaceInviteStatus;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceInviteCreateRequest {
|
||||
username: string; // без @
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Projects (Target Channels)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type ProjectStatus = "active" | "inactive" | "archived";
|
||||
|
||||
export interface Project {
|
||||
id: string; // UUID
|
||||
telegram_id: number;
|
||||
title: string;
|
||||
username: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
// Расширенная версия с дополнительными полями (пока не реализовано на бекенде)
|
||||
export interface TargetChannelDetail extends TargetChannel {
|
||||
// В будущем можно добавить статистику
|
||||
}
|
||||
|
||||
export interface TargetChannelUpdateRequest {
|
||||
is_active?: boolean;
|
||||
}
|
||||
|
||||
export interface TargetChannelsListResponse {
|
||||
target_channels: TargetChannel[];
|
||||
status: ProjectStatus;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// External Channel
|
||||
// Channels (Catalog for Placements)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Основная структура как возвращает API
|
||||
export interface ExternalChannel {
|
||||
export type ChannelStatus = "active" | "inactive" | "archived";
|
||||
|
||||
export interface Channel {
|
||||
id: string; // UUID
|
||||
telegram_id: number;
|
||||
title: string;
|
||||
username: string | null;
|
||||
description: string | null;
|
||||
subscribers_count: number | null;
|
||||
}
|
||||
|
||||
export interface ExternalChannelCreateRequest {
|
||||
telegram_id: number;
|
||||
title: string;
|
||||
username?: string | null;
|
||||
description?: string | null;
|
||||
subscribers_count?: number | null;
|
||||
target_channel_ids: string[]; // UUID[]
|
||||
subscribers_count?: number;
|
||||
}
|
||||
|
||||
export interface ExternalChannelUpdateRequest {
|
||||
title?: string;
|
||||
username?: string | null;
|
||||
description?: string | null;
|
||||
subscribers_count?: number | null;
|
||||
}
|
||||
|
||||
export interface ExternalChannelLinksUpdateRequest {
|
||||
add_target_channel_ids?: string[];
|
||||
remove_target_channel_ids?: string[];
|
||||
}
|
||||
|
||||
export interface ExternalChannelsListResponse {
|
||||
external_channels: ExternalChannel[];
|
||||
export interface ChannelsQueryParams extends PaginationParams {
|
||||
username?: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Creative
|
||||
// Purchase Plan
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type PurchasePlanStatus = "planned" | "completed" | "cancelled";
|
||||
|
||||
export interface PurchasePlanChannel {
|
||||
id: string; // UUID
|
||||
status: PurchasePlanStatus;
|
||||
planned_cost: number | null;
|
||||
comment: string | null;
|
||||
channel: {
|
||||
id: string;
|
||||
telegram_id: number;
|
||||
title: string;
|
||||
username: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface PurchasePlanChannelCreateRequest {
|
||||
channel_id: string;
|
||||
planned_cost?: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface PurchasePlanChannelUpdateRequest {
|
||||
planned_cost?: number;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Creatives
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type CreativeStatus = "active" | "archived";
|
||||
|
||||
export interface Creative {
|
||||
id: string;
|
||||
id: string; // UUID
|
||||
name: string;
|
||||
text: string;
|
||||
target_channel_id: string;
|
||||
target_channel_title: string;
|
||||
text: string; // Contains {invite_link} placeholder
|
||||
project_id: string;
|
||||
project_channel_title: string;
|
||||
created_at: string;
|
||||
status: CreativeStatus;
|
||||
placements_count: number;
|
||||
}
|
||||
|
||||
export interface CreativesListResponse {
|
||||
creatives: Creative[];
|
||||
}
|
||||
|
||||
export interface CreativeCreateRequest {
|
||||
name: string;
|
||||
text: string;
|
||||
target_channel_id: string;
|
||||
}
|
||||
|
||||
export interface CreativeUpdateRequest {
|
||||
name?: string;
|
||||
text?: string;
|
||||
status?: CreativeStatus;
|
||||
}
|
||||
|
||||
export interface CreativesQueryParams extends PaginationParams {
|
||||
project_id?: string;
|
||||
include_archived?: boolean;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Placement (Purchase)
|
||||
// Placements
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type InviteLinkType = "public" | "approval";
|
||||
export type PlacementStatus = "active" | "archived";
|
||||
export type ViewsAvailability =
|
||||
export type InviteLinkType = "public" | "approval";
|
||||
export type PostViewsAvailability =
|
||||
| "unknown"
|
||||
| "available"
|
||||
| "unavailable"
|
||||
| "manual";
|
||||
|
||||
export interface Placement {
|
||||
id: string;
|
||||
target_channel_id: string;
|
||||
target_channel_title: string;
|
||||
external_channel_id: string;
|
||||
external_channel_title: string;
|
||||
id: string; // UUID
|
||||
project_id: string;
|
||||
project_channel_title: string;
|
||||
placement_channel_id: string;
|
||||
placement_channel_title: string;
|
||||
creative_id: string;
|
||||
creative_name: string;
|
||||
placement_date: string;
|
||||
placement_date: string; // ISO 8601
|
||||
cost: number | null;
|
||||
comment: string | null;
|
||||
ad_post_url: string | null;
|
||||
@@ -161,84 +233,96 @@ export interface Placement {
|
||||
invite_link: string;
|
||||
status: PlacementStatus;
|
||||
subscriptions_count: number;
|
||||
views_count: number | null;
|
||||
views_availability: ViewsAvailability;
|
||||
last_views_fetch_at: string | null;
|
||||
views_count?: number | null;
|
||||
views_availability?: PostViewsAvailability;
|
||||
last_views_fetch_at?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PlacementsListResponse {
|
||||
placements: Placement[];
|
||||
}
|
||||
|
||||
export interface PlacementCreateRequest {
|
||||
target_channel_id: string;
|
||||
external_channel_id: string;
|
||||
project_id: string;
|
||||
placement_channel_id: string;
|
||||
creative_id: string;
|
||||
placement_date: string;
|
||||
cost?: number | null;
|
||||
comment?: string | null;
|
||||
ad_post_url?: string | null;
|
||||
invite_link_type?: InviteLinkType;
|
||||
placement_date: string; // ISO 8601
|
||||
cost?: number;
|
||||
comment?: string;
|
||||
ad_post_url?: string;
|
||||
invite_link_type: InviteLinkType;
|
||||
}
|
||||
|
||||
export interface PlacementUpdateRequest {
|
||||
placement_date?: string;
|
||||
cost?: number | null;
|
||||
comment?: string | null;
|
||||
ad_post_url?: string | null;
|
||||
status?: PlacementStatus;
|
||||
cost?: number;
|
||||
comment?: string;
|
||||
ad_post_url?: string;
|
||||
}
|
||||
|
||||
// Legacy aliases for backwards compatibility
|
||||
export type Purchase = Placement;
|
||||
export type PurchaseCreateRequest = PlacementCreateRequest;
|
||||
export type PurchaseUpdateRequest = PlacementUpdateRequest;
|
||||
|
||||
export interface ViewsFetchResponse {
|
||||
placement_id: string;
|
||||
views_count: number | null;
|
||||
views_availability: ViewsAvailability;
|
||||
fetched_at: string;
|
||||
error_message: string | null;
|
||||
}
|
||||
|
||||
export interface ViewsHistoryResponse {
|
||||
histories: ViewsSnapshot[];
|
||||
export interface PlacementsQueryParams extends PaginationParams {
|
||||
project_id?: string;
|
||||
placement_channel_id?: string;
|
||||
creative_id?: string;
|
||||
include_archived?: boolean;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Subscription
|
||||
// Views History
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
purchase_id: string;
|
||||
telegram_user_id: number;
|
||||
user_first_name: string;
|
||||
user_last_name: string | null;
|
||||
user_username: string | null;
|
||||
subscribed_at: string; // ISO 8601
|
||||
is_active: boolean;
|
||||
event_type: "chat_member" | "join_request";
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Views Snapshot
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface ViewsSnapshot {
|
||||
id: string;
|
||||
purchase_id: string;
|
||||
views: number;
|
||||
export interface ViewsHistoryItem {
|
||||
id: string; // UUID
|
||||
views_count: number;
|
||||
fetched_at: string; // ISO 8601
|
||||
}
|
||||
|
||||
export interface ViewsHistoryQueryParams extends PaginationParams {
|
||||
from_date?: string;
|
||||
to_date?: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Analytics
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export type DateGrouping = "day" | "week" | "month" | "quarter" | "year";
|
||||
export type DateGrouping = "day" | "week" | "month" | "DAY" | "WEEK" | "MONTH";
|
||||
|
||||
// Placements Analytics
|
||||
export interface PlacementAnalyticsItem {
|
||||
placement_id: string;
|
||||
placement_channel_title: string;
|
||||
placement_channel_username: string | null;
|
||||
creative_name: string;
|
||||
placement_date: string;
|
||||
cost: number | null;
|
||||
subscriptions_count: number;
|
||||
views_count: number | null;
|
||||
cps: number | null; // cost per subscription
|
||||
cpm: number | null; // cost per mille views
|
||||
}
|
||||
|
||||
// Creatives Analytics (matches actual API response)
|
||||
export interface CreativeAnalyticsItem {
|
||||
id: string;
|
||||
name: string;
|
||||
placements_count: number;
|
||||
total_cost: number;
|
||||
total_subscriptions: number;
|
||||
total_views: number;
|
||||
avg_cpf: number | null; // cost per follower (same as CPS)
|
||||
avg_cpm: number | null;
|
||||
}
|
||||
|
||||
// Channels Analytics
|
||||
export interface ChannelAnalyticsItem {
|
||||
channel_id: string;
|
||||
channel_title: string;
|
||||
channel_username: string | null;
|
||||
placements_count: number;
|
||||
total_cost: number;
|
||||
total_subscriptions: number;
|
||||
total_views: number;
|
||||
avg_cps: number | null;
|
||||
avg_cpm: number | null;
|
||||
}
|
||||
|
||||
// Spending Analytics
|
||||
export interface SpendingDataPoint {
|
||||
@@ -251,40 +335,7 @@ export interface SpendingDataPoint {
|
||||
}
|
||||
|
||||
export interface SpendingAnalytics {
|
||||
total_cost: number;
|
||||
total_subscriptions: number;
|
||||
total_views: number;
|
||||
avg_cpf: number | null;
|
||||
avg_cpm: number | null;
|
||||
chart_data: SpendingDataPoint[];
|
||||
}
|
||||
|
||||
// Placements Analytics
|
||||
export interface PlacementAnalytics {
|
||||
id: string;
|
||||
target_channel_id: string;
|
||||
target_channel_title: string;
|
||||
external_channel_id: string;
|
||||
external_channel_title: string;
|
||||
creative_id: string;
|
||||
creative_name: string;
|
||||
placement_date: string;
|
||||
cost: number | null;
|
||||
subscriptions_count: number;
|
||||
views_count: number | null;
|
||||
cpf: number | null;
|
||||
cpm: number | null;
|
||||
}
|
||||
|
||||
export interface PlacementsAnalytics {
|
||||
placements: PlacementAnalytics[];
|
||||
}
|
||||
|
||||
// Creatives Analytics
|
||||
export interface CreativeAnalytics {
|
||||
id: string;
|
||||
name: string;
|
||||
placements_count: number;
|
||||
total_cost: number;
|
||||
total_subscriptions: number;
|
||||
total_views: number;
|
||||
@@ -292,99 +343,34 @@ export interface CreativeAnalytics {
|
||||
avg_cpm: number | null;
|
||||
}
|
||||
|
||||
export interface CreativesAnalytics {
|
||||
creatives: CreativeAnalytics[];
|
||||
}
|
||||
|
||||
// External Channels Analytics
|
||||
export interface ExternalChannelAnalytics {
|
||||
id: string;
|
||||
title: string;
|
||||
username: string | null;
|
||||
placements_count: number;
|
||||
total_cost: number;
|
||||
total_subscriptions: number;
|
||||
total_views: number;
|
||||
avg_cpf: number | null;
|
||||
avg_cpm: number | null;
|
||||
}
|
||||
|
||||
export interface ExternalChannelsAnalytics {
|
||||
external_channels: ExternalChannelAnalytics[];
|
||||
}
|
||||
|
||||
// Analytics Configuration Types
|
||||
export type AnalyticsMetric = "cost" | "subscriptions" | "views" | "cpf" | "cpm";
|
||||
|
||||
export interface ChartConfig {
|
||||
id: string;
|
||||
targetChannelIds: string[]; // Multiple selection
|
||||
metrics: AnalyticsMetric[];
|
||||
dateFrom: Date | null;
|
||||
dateTo: Date | null;
|
||||
grouping: DateGrouping;
|
||||
export interface AnalyticsQueryParams extends PaginationParams {
|
||||
project_id?: string;
|
||||
}
|
||||
|
||||
export interface SpendingAnalyticsQueryParams {
|
||||
target_channel_id?: string;
|
||||
project_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
grouping?: DateGrouping;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Common API Responses
|
||||
// Common Types
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface ListResponse<T> {
|
||||
data: T[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface SuccessResponse {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
details?: any;
|
||||
};
|
||||
detail: string;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Query Parameters
|
||||
// Legacy Aliases (for backwards compatibility during migration)
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface TargetChannelQueryParams {
|
||||
is_active?: boolean;
|
||||
}
|
||||
/** @deprecated Use Project instead */
|
||||
export type TargetChannel = Project;
|
||||
|
||||
export interface ExternalChannelQueryParams {
|
||||
target_channel_id?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface CreativeQueryParams {
|
||||
target_channel_id?: string;
|
||||
is_archived?: boolean;
|
||||
}
|
||||
|
||||
export interface PlacementQueryParams {
|
||||
target_channel_id?: string;
|
||||
external_channel_id?: string;
|
||||
creative_id?: string;
|
||||
is_archived?: boolean;
|
||||
date_from?: string; // ISO 8601
|
||||
date_to?: string; // ISO 8601
|
||||
sort?: "date" | "cost" | "cpf" | "subscriptions";
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface AnalyticsOverviewQueryParams {
|
||||
target_channel_id?: string;
|
||||
date_from?: string;
|
||||
date_to?: string;
|
||||
}
|
||||
/** @deprecated Use Channel instead */
|
||||
export type ExternalChannel = Channel;
|
||||
|
||||
/** @deprecated Use PlacementCreateRequest with new fields */
|
||||
export type PurchaseCreateRequest = PlacementCreateRequest;
|
||||
|
||||
@@ -14,40 +14,8 @@ export const STORAGE_KEYS = {
|
||||
ACCESS_TOKEN: "tgex_access_token",
|
||||
REFRESH_TOKEN: "tgex_refresh_token",
|
||||
USER: "tgex_user",
|
||||
SELECTED_TARGET_CHANNEL: "tgex_selected_target_channel",
|
||||
} 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",
|
||||
SELECTED_WORKSPACE: "tgex_selected_workspace",
|
||||
SELECTED_PROJECT: "tgex_selected_project",
|
||||
} as const;
|
||||
|
||||
// Error Codes
|
||||
@@ -57,11 +25,6 @@ export const ERROR_CODES = {
|
||||
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
|
||||
@@ -72,34 +35,57 @@ export const DATE_FORMATS = {
|
||||
} as const;
|
||||
|
||||
// Pagination
|
||||
export const DEFAULT_PAGE_SIZE = 20;
|
||||
export const DEFAULT_PAGE_SIZE = 50;
|
||||
export const PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
||||
|
||||
// Routes
|
||||
// Routes Builder
|
||||
export const buildRoute = {
|
||||
// Dashboard
|
||||
dashboard: (workspaceId: string) => `/dashboard/${workspaceId}`,
|
||||
|
||||
// Projects
|
||||
projects: (workspaceId: string) => `/dashboard/${workspaceId}/projects`,
|
||||
|
||||
// Channels
|
||||
channels: (workspaceId: string) => `/dashboard/${workspaceId}/channels`,
|
||||
|
||||
// Purchase Plan
|
||||
purchasePlans: (workspaceId: string) => `/dashboard/${workspaceId}/purchase-plans`,
|
||||
purchasePlan: (workspaceId: string, projectId: string) =>
|
||||
`/dashboard/${workspaceId}/purchase-plans/${projectId}`,
|
||||
|
||||
// Creatives
|
||||
creatives: (workspaceId: string) => `/dashboard/${workspaceId}/creatives`,
|
||||
creativeNew: (workspaceId: string) => `/dashboard/${workspaceId}/creatives/new`,
|
||||
creativeDetail: (workspaceId: string, creativeId: string) =>
|
||||
`/dashboard/${workspaceId}/creatives/${creativeId}`,
|
||||
|
||||
// Placements
|
||||
placements: (workspaceId: string) => `/dashboard/${workspaceId}/placements`,
|
||||
placementNew: (workspaceId: string) => `/dashboard/${workspaceId}/placements/new`,
|
||||
placementDetail: (workspaceId: string, placementId: string) =>
|
||||
`/dashboard/${workspaceId}/placements/${placementId}`,
|
||||
|
||||
// Analytics
|
||||
analytics: (workspaceId: string) => `/dashboard/${workspaceId}/analytics`,
|
||||
analyticsSpending: (workspaceId: string) =>
|
||||
`/dashboard/${workspaceId}/analytics/spending`,
|
||||
analyticsCreatives: (workspaceId: string) =>
|
||||
`/dashboard/${workspaceId}/analytics/creatives`,
|
||||
analyticsChannels: (workspaceId: string) =>
|
||||
`/dashboard/${workspaceId}/analytics/channels`,
|
||||
analyticsPlacements: (workspaceId: string) =>
|
||||
`/dashboard/${workspaceId}/analytics/placements`,
|
||||
|
||||
// Settings
|
||||
settings: (workspaceId: string) => `/dashboard/${workspaceId}/settings`,
|
||||
members: (workspaceId: string) => `/dashboard/${workspaceId}/settings/members`,
|
||||
invites: (workspaceId: string) => `/dashboard/${workspaceId}/settings/invites`,
|
||||
} as const;
|
||||
|
||||
// Static 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;
|
||||
|
||||
Reference in New Issue
Block a user