feat: global ui & functional update

This commit is contained in:
ivannoskov
2026-01-21 22:43:54 +03:00
parent 8958d89d12
commit 619fd5c1ef
32 changed files with 3803 additions and 1325 deletions

View File

@@ -15,13 +15,14 @@ import type {
OverviewAnalyticsQueryParams,
ProjectsAnalyticsResponse,
ProjectsAnalyticsQueryParams,
PlacementsAnalyticsQueryParams,
} from "@/lib/types/api";
export const analyticsApi = {
/**
* Get placements analytics
*/
placements: (workspaceId: string, params?: AnalyticsQueryParams) =>
placements: (workspaceId: string, params?: PlacementsAnalyticsQueryParams) =>
api.get<PaginatedResponse<PlacementAnalyticsItem>>(
`/workspaces/${workspaceId}/analytics/placements`,
{ params }

View File

@@ -81,15 +81,20 @@ export const apiRequest = async <T = any>(
options: RequestOptions = {}
): Promise<T> => {
const { params, requireAuth = true, ...fetchOptions } = options;
const body = fetchOptions.body;
// Build URL
const url = buildUrl(`${API_URL}${endpoint}`, params);
const headers: HeadersInit = {
"Content-Type": "application/json",
...fetchOptions.headers,
};
// Don't set Content-Type for FormData - browser will set it with boundary
if (!(body instanceof FormData)) {
(headers as Record<string, string>)["Content-Type"] = "application/json";
}
// Add auth token if required
if (requireAuth) {
const token = getAuthToken();
@@ -99,9 +104,16 @@ export const apiRequest = async <T = any>(
}
try {
// Serialize body to JSON if it's an object (not FormData or string)
let serializedBody = body;
if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof String)) {
serializedBody = JSON.stringify(body);
}
const response = await fetch(url, {
...fetchOptions,
headers,
body: serializedBody,
});
// Handle 204 No Content
@@ -145,21 +157,21 @@ export const api = {
apiRequest<T>(endpoint, {
...options,
method: "POST",
body: body ? JSON.stringify(body) : undefined,
body,
}),
put: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
apiRequest<T>(endpoint, {
...options,
method: "PUT",
body: body ? JSON.stringify(body) : undefined,
body,
}),
patch: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
apiRequest<T>(endpoint, {
...options,
method: "PATCH",
body: body ? JSON.stringify(body) : undefined,
body,
}),
delete: <T = any>(endpoint: string, options?: RequestOptions) =>

View File

@@ -21,6 +21,15 @@ export const creativesApi = {
{ params }
),
/**
* Get list of creatives for a specific project
*/
listByProject: (workspaceId: string, projectId: string) =>
api.get<PaginatedResponse<Creative>>(
`/workspaces/${workspaceId}/creatives`,
{ params: { project_id: projectId } }
),
/**
* Get single creative
*/

View File

@@ -5,6 +5,7 @@
import { api } from "./client";
import type {
Placement,
PlacementWithStats,
PlacementCreateRequest,
PlacementUpdateRequest,
PlacementsQueryParams,
@@ -23,6 +24,13 @@ export const placementsApi = {
{ params }
),
/**
* Get placements for a specific project (grouped by channel UI)
*/
getByProject: (workspaceId: string, projectId: string) =>
api.get<{ placements: Placement[] }>(`/workspaces/${workspaceId}/projects/${projectId}/placements`)
.then((response) => response.placements),
/**
* Get single placement
*/
@@ -35,6 +43,46 @@ export const placementsApi = {
create: (workspaceId: string, data: PlacementCreateRequest) =>
api.post<Placement>(`/workspaces/${workspaceId}/placements`, data),
/**
* Create placements for multiple channels (bulk creation)
*/
createBulk: (
workspaceId: string,
projectId: string,
data: {
creative_id?: string;
channels: Array<{
username: string;
status?: string;
comment?: string;
details?: {
placement_at?: string;
payment_at?: string;
cost?: { type: string; value: number };
cost_before_bargain?: number;
placement_type?: string;
format?: string;
comment?: string;
creative_id?: string;
};
}>;
details?: {
placement_at?: string;
payment_at?: string;
cost?: { type: string; value: number };
cost_before_bargain?: number;
placement_type?: string;
format?: string;
comment?: string;
creative_id?: string;
};
}
) =>
api.post<{ placements: Placement[] }>(
`/workspaces/${workspaceId}/projects/${projectId}/placements`,
data
),
/**
* Update placement
*/

View File

@@ -9,6 +9,10 @@ export interface UpdateProjectInviteLinkTypeRequest {
purchase_invite_type_default: "public" | "approval";
}
export interface MoveProjectRequest {
target_workspace_id: string;
}
export const projectsApi = {
/**
* Get list of projects for workspace
@@ -41,10 +45,31 @@ export const projectsApi = {
`/workspaces/${workspaceId}/projects/${projectId}/archive`
),
/**
* Unarchive project
*/
unarchive: (workspaceId: string, projectId: string) =>
api.post<Project>(
`/workspaces/${workspaceId}/projects/${projectId}/unarchive`
),
/**
* Delete project (soft delete)
*/
delete: (workspaceId: string, projectId: string) =>
api.delete(`/workspaces/${workspaceId}/projects/${projectId}`),
/**
* Move project to another workspace
*/
move: (
workspaceId: string,
projectId: string,
data: MoveProjectRequest
) =>
api.put<Project>(
`/workspaces/${workspaceId}/projects/${projectId}/move`,
data
),
};

View File

@@ -37,5 +37,22 @@ export const workspacesApi = {
*/
delete: (workspaceId: string) =>
api.delete<void>(`${BASE_PATH}/${workspaceId}`),
/**
* Upload workspace avatar
*/
uploadAvatar: (workspaceId: string, file: File) => {
const formData = new FormData();
formData.append("file", file);
return api.post<Workspace>(`${BASE_PATH}/${workspaceId}/avatar`, formData, {
headers: { "Content-Type": "multipart/form-data" },
});
},
/**
* Delete workspace avatar
*/
deleteAvatar: (workspaceId: string) =>
api.delete<Workspace>(`${BASE_PATH}/${workspaceId}/avatar`),
};