This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,128 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse, PaginatedData } from "@/types/api-response";
|
||||
import type {
|
||||
AdminPaginationParams,
|
||||
AdminUserDto,
|
||||
AnalyticsOverviewDto,
|
||||
SettingDto,
|
||||
UpdateSettingDto,
|
||||
UpdateUserRoleDto,
|
||||
UpdateUserSubscriptionDto,
|
||||
UsageLimitDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Admin Service
|
||||
* Backend: /api/admin/*
|
||||
*/
|
||||
|
||||
// Analytics
|
||||
const getAnalyticsOverview = () => {
|
||||
return apiRequest<ApiResponse<AnalyticsOverviewDto>>({
|
||||
url: "/admin/analytics/overview",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
// Settings
|
||||
const getAllSettings = () => {
|
||||
return apiRequest<ApiResponse<Record<string, string>>>({
|
||||
url: "/admin/settings",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const updateSetting = (key: string, dto: UpdateSettingDto) => {
|
||||
return apiRequest<ApiResponse<SettingDto>>({
|
||||
url: `/admin/settings/${key}`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
// Usage Limits
|
||||
const getAllUsageLimits = (params?: AdminPaginationParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedData<UsageLimitDto>>>({
|
||||
url: "/admin/usage-limits",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const resetAllUsageLimits = () => {
|
||||
return apiRequest<ApiResponse<{ count: number }>>({
|
||||
url: "/admin/usage-limits/reset-all",
|
||||
client: "admin",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
const getAllUsers = (params?: AdminPaginationParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedData<AdminUserDto>>>({
|
||||
url: "/admin/users",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const getUserById = (id: string) => {
|
||||
return apiRequest<ApiResponse<AdminUserDto>>({
|
||||
url: `/admin/users/${id}`,
|
||||
client: "admin",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const deleteUser = (id: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/users/${id}`,
|
||||
client: "admin",
|
||||
method: "delete",
|
||||
});
|
||||
};
|
||||
|
||||
const updateUserRole = (id: string, dto: UpdateUserRoleDto) => {
|
||||
return apiRequest<ApiResponse<AdminUserDto>>({
|
||||
url: `/admin/users/${id}/role`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const updateUserSubscription = (id: string, dto: UpdateUserSubscriptionDto) => {
|
||||
return apiRequest<ApiResponse<AdminUserDto>>({
|
||||
url: `/admin/users/${id}/subscription`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleUserActive = (id: string) => {
|
||||
return apiRequest<ApiResponse<AdminUserDto>>({
|
||||
url: `/admin/users/${id}/toggle-active`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
});
|
||||
};
|
||||
|
||||
export const adminService = {
|
||||
getAnalyticsOverview,
|
||||
getAllSettings,
|
||||
updateSetting,
|
||||
getAllUsageLimits,
|
||||
resetAllUsageLimits,
|
||||
getAllUsers,
|
||||
getUserById,
|
||||
deleteUser,
|
||||
updateUserRole,
|
||||
updateUserSubscription,
|
||||
toggleUserActive,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { PaginationDto } from "@/types/api-response";
|
||||
|
||||
// ========================
|
||||
// Shared Query Params
|
||||
// ========================
|
||||
|
||||
export interface AdminPaginationParams extends PaginationDto {
|
||||
sortBy?: string;
|
||||
sortOrder?: "asc" | "desc";
|
||||
search?: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Settings
|
||||
// ========================
|
||||
|
||||
export interface SettingDto {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateSettingDto {
|
||||
value: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Usage Limits
|
||||
// ========================
|
||||
|
||||
export interface UsageLimitDto {
|
||||
id: string;
|
||||
userId: string;
|
||||
feature: string;
|
||||
used: number;
|
||||
limit: number;
|
||||
resetAt?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Admin Users
|
||||
// ========================
|
||||
|
||||
export interface AdminUserDto {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
role?: string;
|
||||
isActive: boolean;
|
||||
subscription?: string;
|
||||
createdAt: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UpdateUserRoleDto {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserSubscriptionDto {
|
||||
subscription: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Analytics
|
||||
// ========================
|
||||
|
||||
export interface AnalyticsOverviewDto {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
totalPredictions: number;
|
||||
totalCoupons: number;
|
||||
aiHealth?: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { adminService } from "./service";
|
||||
import type {
|
||||
AdminPaginationParams,
|
||||
UpdateSettingDto,
|
||||
UpdateUserRoleDto,
|
||||
UpdateUserSubscriptionDto,
|
||||
} from "./types";
|
||||
|
||||
export const AdminQueryKeys = {
|
||||
all: ["admin"] as const,
|
||||
analytics: () => [...AdminQueryKeys.all, "analytics"] as const,
|
||||
settings: () => [...AdminQueryKeys.all, "settings"] as const,
|
||||
usageLimits: (params?: AdminPaginationParams) =>
|
||||
[...AdminQueryKeys.all, "usageLimits", params] as const,
|
||||
users: (params?: AdminPaginationParams) =>
|
||||
[...AdminQueryKeys.all, "users", params] as const,
|
||||
user: (id: string) => [...AdminQueryKeys.all, "user", id] as const,
|
||||
};
|
||||
|
||||
// Analytics
|
||||
export const useAdminAnalytics = () => {
|
||||
return useQuery({
|
||||
queryKey: AdminQueryKeys.analytics(),
|
||||
queryFn: () => adminService.getAnalyticsOverview(),
|
||||
});
|
||||
};
|
||||
|
||||
// Settings
|
||||
export const useAdminSettings = () => {
|
||||
return useQuery({
|
||||
queryKey: AdminQueryKeys.settings(),
|
||||
queryFn: () => adminService.getAllSettings(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSetting = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ key, dto }: { key: string; dto: UpdateSettingDto }) =>
|
||||
adminService.updateSetting(key, dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.settings() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Usage Limits
|
||||
export const useAdminUsageLimits = (params?: AdminPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: AdminQueryKeys.usageLimits(params),
|
||||
queryFn: () => adminService.getAllUsageLimits(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useResetAllUsageLimits = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => adminService.resetAllUsageLimits(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usageLimits() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Users
|
||||
export const useAdminUsers = (params?: AdminPaginationParams) => {
|
||||
return useQuery({
|
||||
queryKey: AdminQueryKeys.users(params),
|
||||
queryFn: () => adminService.getAllUsers(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useAdminUserById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: AdminQueryKeys.user(id),
|
||||
queryFn: () => adminService.getUserById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteUser = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => adminService.deleteUser(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserRole = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserRoleDto }) =>
|
||||
adminService.updateUserRole(id, dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserSubscription = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, dto }: { id: string; dto: UpdateUserSubscriptionDto }) =>
|
||||
adminService.updateUserSubscription(id, dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useToggleUserActive = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => adminService.toggleUserActive(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import type { AnalyzeMatchesDto, AnalysisResultDto, AnalysisHistoryDto } from "./types";
|
||||
|
||||
/**
|
||||
* Analysis Service
|
||||
* Backend: /api/analysis/*
|
||||
*/
|
||||
|
||||
const analyzeMatches = (dto: AnalyzeMatchesDto) => {
|
||||
return apiRequest<ApiResponse<AnalysisResultDto>>({
|
||||
url: "/analysis/analyze",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const getHistory = () => {
|
||||
return apiRequest<ApiResponse<AnalysisHistoryDto>>({
|
||||
url: "/analysis/history",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const analysisService = {
|
||||
analyzeMatches,
|
||||
getHistory,
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface AnalyzeMatchesDto {
|
||||
matchIds: string[];
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface AnalysisResultDto {
|
||||
id: string;
|
||||
matchIds: string[];
|
||||
result: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AnalysisHistoryDto {
|
||||
analyses: AnalysisResultDto[];
|
||||
total: number;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { analysisService } from "./service";
|
||||
import type { AnalyzeMatchesDto } from "./types";
|
||||
|
||||
export const AnalysisQueryKeys = {
|
||||
all: ["analysis"] as const,
|
||||
history: () => [...AnalysisQueryKeys.all, "history"] as const,
|
||||
};
|
||||
|
||||
export const useAnalyzeMatches = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: AnalyzeMatchesDto) => analysisService.analyzeMatches(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useAnalysisHistory = () => {
|
||||
return useQuery({
|
||||
queryKey: AnalysisQueryKeys.history(),
|
||||
queryFn: () => analysisService.getHistory(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
|
||||
/**
|
||||
* Custom error class for API errors with extracted message
|
||||
* Re-exported from create-api-client for convenience
|
||||
*/
|
||||
export { ApiError } from "@/lib/api/create-api-client";
|
||||
|
||||
/**
|
||||
* API Error item structure from backend errors array
|
||||
*/
|
||||
export interface ApiErrorItem {
|
||||
message: string;
|
||||
members?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a user-friendly error message from API response.
|
||||
* Prioritizes `errors` array messages, falls back to `message` field.
|
||||
*
|
||||
* @param response - The API response object
|
||||
* @param fallbackMessage - Default message if no error info available
|
||||
* @returns A formatted error message string
|
||||
*/
|
||||
export function extractApiErrorMessage(
|
||||
response: Partial<ApiResponse> | null | undefined,
|
||||
fallbackMessage = "Bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
): string {
|
||||
if (!response) {
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
// Check if errors array exists and has items
|
||||
if (
|
||||
response.errors &&
|
||||
Array.isArray(response.errors) &&
|
||||
response.errors.length > 0
|
||||
) {
|
||||
// Extract messages from errors array
|
||||
const errorMessages = response.errors
|
||||
.map((error: ApiErrorItem | string) => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
return error?.message || "";
|
||||
})
|
||||
.filter((msg) => msg.length > 0);
|
||||
|
||||
if (errorMessages.length > 0) {
|
||||
return errorMessages.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to message field
|
||||
if (response.message && response.message.length > 0) {
|
||||
return response.message;
|
||||
}
|
||||
|
||||
return fallbackMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts error messages as an array from API response.
|
||||
* Useful when you need to display multiple errors separately.
|
||||
*
|
||||
* @param response - The API response object
|
||||
* @param fallbackMessage - Default message if no error info available
|
||||
* @returns An array of error message strings
|
||||
*/
|
||||
export function extractApiErrorMessages(
|
||||
response: Partial<ApiResponse> | null | undefined,
|
||||
fallbackMessage = "Bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
): string[] {
|
||||
if (!response) {
|
||||
return [fallbackMessage];
|
||||
}
|
||||
|
||||
// Check if errors array exists and has items
|
||||
if (
|
||||
response.errors &&
|
||||
Array.isArray(response.errors) &&
|
||||
response.errors.length > 0
|
||||
) {
|
||||
const errorMessages = response.errors
|
||||
.map((error: ApiErrorItem | string) => {
|
||||
if (typeof error === "string") {
|
||||
return error;
|
||||
}
|
||||
return error?.message || "";
|
||||
})
|
||||
.filter((msg) => msg.length > 0);
|
||||
|
||||
if (errorMessages.length > 0) {
|
||||
return errorMessages;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to message field
|
||||
if (response.message && response.message.length > 0) {
|
||||
return [response.message];
|
||||
}
|
||||
|
||||
return [fallbackMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an API response represents a validation error (422)
|
||||
*/
|
||||
export function isValidationError(
|
||||
response: Partial<ApiResponse> | null | undefined,
|
||||
): boolean {
|
||||
return response?.status === 422;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets field-specific error messages from API validation errors
|
||||
* Returns a map of field names to their error messages
|
||||
*/
|
||||
export function getFieldErrors(
|
||||
response: Partial<ApiResponse> | null | undefined,
|
||||
): Record<string, string[]> {
|
||||
const fieldErrors: Record<string, string[]> = {};
|
||||
|
||||
if (!response?.errors || !Array.isArray(response.errors)) {
|
||||
return fieldErrors;
|
||||
}
|
||||
|
||||
response.errors.forEach((error: ApiErrorItem) => {
|
||||
if (error.members && Array.isArray(error.members)) {
|
||||
error.members.forEach((fieldName: string) => {
|
||||
if (!fieldErrors[fieldName]) {
|
||||
fieldErrors[fieldName] = [];
|
||||
}
|
||||
if (error.message) {
|
||||
fieldErrors[fieldName].push(error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return fieldErrors;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { clientMap } from '@/lib/api/client-map';
|
||||
import { Method } from 'axios';
|
||||
|
||||
interface ApiRequestOptions {
|
||||
url: string;
|
||||
client: keyof typeof clientMap;
|
||||
method?: Method;
|
||||
data?: unknown;
|
||||
params?: Record<string, string | number | boolean | undefined> | object;
|
||||
}
|
||||
|
||||
export async function apiRequest<T = unknown>(options: ApiRequestOptions): Promise<T> {
|
||||
const { url, client, method = 'get', data, params } = options;
|
||||
const clientInstance = clientMap[client];
|
||||
|
||||
if (!url || !clientInstance) {
|
||||
throw new Error(`Invalid API request: ${client} - ${url}`);
|
||||
}
|
||||
|
||||
const response = await clientInstance.request<T>({ method, url, data, params });
|
||||
return response.data;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import baseUrl from '@/config/base-url';
|
||||
import { createApiClient } from '@/lib/api/create-api-client';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
export const clientMap: Record<keyof typeof baseUrl, AxiosInstance> = {
|
||||
auth: createApiClient(baseUrl.auth!),
|
||||
admin: createApiClient(baseUrl.admin!),
|
||||
core: createApiClient(baseUrl.core!),
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
CreateCouponDto,
|
||||
SuggestCouponDto,
|
||||
AnalyzeMatchDto,
|
||||
CouponResponseDto,
|
||||
CouponHistoryDto,
|
||||
UserBettingStatsDto,
|
||||
MatchAnalysisResultDto,
|
||||
DailyBankoResponseDto,
|
||||
SmartCouponResultDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Coupons Service
|
||||
* Backend: /api/coupon/*
|
||||
*/
|
||||
|
||||
const analyzeMatch = (dto: AnalyzeMatchDto) => {
|
||||
return apiRequest<ApiResponse<MatchAnalysisResultDto>>({
|
||||
url: "/coupon/analyze",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const createCoupon = (dto: CreateCouponDto) => {
|
||||
return apiRequest<ApiResponse<CouponResponseDto>>({
|
||||
url: "/coupon",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const getDailyBanko = (matchIds: string[]) => {
|
||||
return apiRequest<ApiResponse<DailyBankoResponseDto>>({
|
||||
url: "/coupon/daily-banko",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: { matchIds },
|
||||
});
|
||||
};
|
||||
|
||||
const getHistory = (limit: number) => {
|
||||
return apiRequest<ApiResponse<CouponHistoryDto>>({
|
||||
url: "/coupon/history",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params: { limit },
|
||||
});
|
||||
};
|
||||
|
||||
const getUserStats = () => {
|
||||
return apiRequest<ApiResponse<UserBettingStatsDto>>({
|
||||
url: "/coupon/my-stats",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const suggestCoupon = (dto: SuggestCouponDto) => {
|
||||
return apiRequest<ApiResponse<SmartCouponResultDto>>({
|
||||
url: "/coupon/suggest",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
export const couponsService = {
|
||||
analyzeMatch,
|
||||
createCoupon,
|
||||
getDailyBanko,
|
||||
getHistory,
|
||||
getUserStats,
|
||||
suggestCoupon,
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { CouponStrategy } from "@/lib/api/predictions/types";
|
||||
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface CreateCouponDto {
|
||||
name?: string;
|
||||
items: CouponItemDto[];
|
||||
strategy?: CouponStrategy;
|
||||
}
|
||||
|
||||
export interface CouponItemDto {
|
||||
matchId: string;
|
||||
matchName?: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
odd: number;
|
||||
}
|
||||
|
||||
export interface SuggestCouponDto {
|
||||
matchIds: string[];
|
||||
strategy?: CouponStrategy;
|
||||
maxMatches?: number;
|
||||
minConfidence?: number;
|
||||
}
|
||||
|
||||
export interface AnalyzeMatchDto {
|
||||
matchIds: string[];
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface CouponResponseDto {
|
||||
id: string;
|
||||
name?: string;
|
||||
items: CouponItemDto[];
|
||||
strategy?: CouponStrategy;
|
||||
totalOdd: number;
|
||||
createdAt: string;
|
||||
status?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CouponHistoryDto {
|
||||
coupons: CouponResponseDto[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UserBettingStatsDto {
|
||||
totalCoupons: number;
|
||||
totalBets: number;
|
||||
wonBets: number;
|
||||
lostBets: number;
|
||||
pendingBets: number;
|
||||
winRate: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MatchAnalysisResultDto {
|
||||
matchId: string;
|
||||
analysis: Record<string, unknown>;
|
||||
predictions: Record<string, unknown>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface DailyBankoResponseDto {
|
||||
picks: Array<{
|
||||
matchId: string;
|
||||
matchName: string;
|
||||
pick: string;
|
||||
confidence: number;
|
||||
odd: number;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
totalOdd: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SuggestedCouponBetDto {
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number;
|
||||
risk_level: string;
|
||||
data_quality: string;
|
||||
}
|
||||
|
||||
export interface SuggestedCouponRejectedMatchDto {
|
||||
match_id: string;
|
||||
reason: string;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export interface SmartCouponResultDto {
|
||||
strategy: CouponStrategy;
|
||||
generated_at: string;
|
||||
match_count: number;
|
||||
bets: SuggestedCouponBetDto[];
|
||||
total_odds: number;
|
||||
expected_win_rate: number;
|
||||
rejected_matches: SuggestedCouponRejectedMatchDto[];
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { couponsService } from "./service";
|
||||
import type {
|
||||
CreateCouponDto,
|
||||
SuggestCouponDto,
|
||||
AnalyzeMatchDto,
|
||||
} from "./types";
|
||||
|
||||
export const CouponsQueryKeys = {
|
||||
all: ["coupons"] as const,
|
||||
history: (limit?: number) =>
|
||||
[...CouponsQueryKeys.all, "history", limit] as const,
|
||||
stats: () => [...CouponsQueryKeys.all, "stats"] as const,
|
||||
};
|
||||
|
||||
export const useAnalyzeMatch = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: AnalyzeMatchDto) => couponsService.analyzeMatch(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateCoupon = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (dto: CreateCouponDto) => couponsService.createCoupon(dto),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: CouponsQueryKeys.all });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const useDailyBanko = () => {
|
||||
return useMutation({
|
||||
mutationFn: (matchIds: string[]) => couponsService.getDailyBanko(matchIds),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCouponHistory = (limit: number = 20) => {
|
||||
return useQuery({
|
||||
queryKey: CouponsQueryKeys.history(limit),
|
||||
queryFn: () => couponsService.getHistory(limit),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserBettingStats = () => {
|
||||
return useQuery({
|
||||
queryKey: CouponsQueryKeys.stats(),
|
||||
queryFn: () => couponsService.getUserStats(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSuggestCoupon = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: SuggestCouponDto) => couponsService.suggestCoupon(dto),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import { toaster } from "@/components/ui/feedback/toaster";
|
||||
|
||||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { getSession, signOut } from "next-auth/react";
|
||||
import { extractApiErrorMessage } from "./api-error-utils";
|
||||
import type { ApiResponse } from "@/types/api-response";
|
||||
|
||||
// const MESSAGES = {
|
||||
// tr: {
|
||||
// title: 'Oturum Süresi Doldu',
|
||||
// description: 'Güvenliğiniz için çıkış yapıldı. Lütfen tekrar giriş yapınız.',
|
||||
// },
|
||||
// en: {
|
||||
// title: 'Session Expired',
|
||||
// description: 'You have been logged out for security reasons. Please log in again.',
|
||||
// },
|
||||
// };
|
||||
|
||||
/**
|
||||
* Custom error class for API errors with extracted message
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
status: number;
|
||||
originalResponse: unknown;
|
||||
|
||||
constructor(message: string, status: number, originalResponse?: unknown) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
this.status = status;
|
||||
this.originalResponse = originalResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Throttle 429 toaster - only show once per 5 seconds
|
||||
let last429ToastTime = 0;
|
||||
const TOAST_THROTTLE_MS = 5000;
|
||||
|
||||
const show429Toast = (message: string) => {
|
||||
const now = Date.now();
|
||||
if (now - last429ToastTime > TOAST_THROTTLE_MS) {
|
||||
last429ToastTime = now;
|
||||
toaster.error({
|
||||
title: "Too Many Requests",
|
||||
description: message || "Lütfen bekleyiniz.",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function createApiClient(baseURL: string): AxiosInstance {
|
||||
const client = axios.create({ baseURL });
|
||||
|
||||
// Helper to get locale from cookie or URL
|
||||
const getLocale = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
// Try to get from cookie first (NEXT_LOCALE is the default cookie name)
|
||||
const cookieLocale = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("NEXT_LOCALE="))
|
||||
?.split("=")[1];
|
||||
|
||||
if (cookieLocale) return cookieLocale;
|
||||
|
||||
// Fallback: get from URL path (e.g., /tr/generator -> tr)
|
||||
const pathLocale = window.location.pathname.split("/")[1];
|
||||
if (["en", "tr", "de"].includes(pathLocale)) return pathLocale;
|
||||
}
|
||||
return "tr"; // Default locale
|
||||
};
|
||||
|
||||
client.interceptors.request.use(async (config) => {
|
||||
const session = await getSession();
|
||||
const token = session?.accessToken;
|
||||
|
||||
if (token) {
|
||||
config.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
// Set Accept-Language header based on current locale
|
||||
const locale = getLocale();
|
||||
config.headers.set("Accept-Language", locale);
|
||||
|
||||
if (!(config.data instanceof FormData)) {
|
||||
config.headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
client.interceptors.response.use(
|
||||
(response) => {
|
||||
// Check for API-level errors (success: false) even on 200 responses
|
||||
const data = response.data;
|
||||
if (
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
"success" in data &&
|
||||
data.success === false
|
||||
) {
|
||||
const errorMessage = extractApiErrorMessage(data, "Bir hata oluştu");
|
||||
|
||||
// Handle 429 in success: false body
|
||||
if (data.status === 429) {
|
||||
show429Toast(errorMessage);
|
||||
}
|
||||
|
||||
// Use API-level status (data.status) if available, otherwise fall back to HTTP status
|
||||
const errorStatus = data.status || response.status;
|
||||
const apiError = new ApiError(errorMessage, errorStatus, data);
|
||||
return Promise.reject(apiError);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
// Handle 429 Too Many Requests
|
||||
if (error.response?.status === 429) {
|
||||
const responseData = error.response?.data as Partial<ApiResponse>;
|
||||
const message =
|
||||
responseData?.message || "Too many requests. Please wait a moment.";
|
||||
show429Toast(message);
|
||||
}
|
||||
|
||||
// Handle 401 Unauthorized
|
||||
if (error.response?.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
const isAuthPath =
|
||||
window.location.pathname.includes("/api/auth") ||
|
||||
window.location.pathname === "/";
|
||||
|
||||
if (!isAuthPath) {
|
||||
await signOut({ redirect: true, callbackUrl: "/" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract error message from API response
|
||||
const responseData = error.response?.data;
|
||||
if (responseData && typeof responseData === "object") {
|
||||
const errorMessage = extractApiErrorMessage(
|
||||
responseData as Partial<ApiResponse>,
|
||||
error.message,
|
||||
);
|
||||
const apiError = new ApiError(
|
||||
errorMessage,
|
||||
error.response?.status || 500,
|
||||
responseData,
|
||||
);
|
||||
return Promise.reject(apiError);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { PermissionResponseDto, CreatePermissionDto } from "./types";
|
||||
|
||||
/**
|
||||
* Admin Permissions Service - Example Implementation
|
||||
* Matches Backend: /api/admin/permissions/*
|
||||
*/
|
||||
|
||||
const getAll = () => {
|
||||
return apiRequest<ApiResponse<PermissionResponseDto[]>>({
|
||||
url: "/admin/permissions",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const create = (data: CreatePermissionDto) => {
|
||||
return apiRequest<ApiResponse<PermissionResponseDto>>({
|
||||
url: "/admin/permissions",
|
||||
client: "admin",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const adminPermissionsService = {
|
||||
getAll,
|
||||
create,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// Permission DTOs - Matches Backend
|
||||
export interface PermissionResponseDto {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// Create Permission
|
||||
export interface CreatePermissionDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { adminPermissionsService } from "./service";
|
||||
import { PermissionResponseDto, CreatePermissionDto } from "./types";
|
||||
|
||||
export const AdminPermissionsQueryKeys = {
|
||||
all: ["admin-permissions"] as const,
|
||||
list: () => [...AdminPermissionsQueryKeys.all, "list"] as const,
|
||||
};
|
||||
|
||||
export function useGetAllPermissions() {
|
||||
const queryKey = AdminPermissionsQueryKeys.list();
|
||||
|
||||
const { data, ...rest } = useQuery<ApiResponse<PermissionResponseDto[]>>({
|
||||
queryKey: queryKey,
|
||||
queryFn: adminPermissionsService.getAll,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useCreatePermission() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<PermissionResponseDto>,
|
||||
Error,
|
||||
CreatePermissionDto
|
||||
>({
|
||||
mutationFn: (permissionData) =>
|
||||
adminPermissionsService.create(permissionData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: AdminPermissionsQueryKeys.all,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
RoleResponseDto,
|
||||
CreateRoleDto,
|
||||
UpdateRoleDto,
|
||||
RolePermissionResponseDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Admin Roles Service - Example Implementation
|
||||
* Matches Backend: /api/admin/roles/*
|
||||
*/
|
||||
|
||||
const getAll = () => {
|
||||
return apiRequest<ApiResponse<RoleResponseDto[]>>({
|
||||
url: "/admin/roles",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const create = (data: CreateRoleDto) => {
|
||||
return apiRequest<ApiResponse<RoleResponseDto>>({
|
||||
url: "/admin/roles",
|
||||
client: "admin",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const update = (roleId: string, data: UpdateRoleDto) => {
|
||||
return apiRequest<ApiResponse<RoleResponseDto>>({
|
||||
url: `/admin/roles/${roleId}`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (roleId: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/roles/${roleId}`,
|
||||
client: "admin",
|
||||
method: "delete",
|
||||
});
|
||||
};
|
||||
|
||||
const assignPermission = (roleId: string, permissionId: string) => {
|
||||
return apiRequest<ApiResponse<RolePermissionResponseDto>>({
|
||||
url: `/admin/roles/${roleId}/permissions/${permissionId}`,
|
||||
client: "admin",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
const removePermission = (roleId: string, permissionId: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/roles/${roleId}/permissions/${permissionId}`,
|
||||
client: "admin",
|
||||
method: "delete",
|
||||
});
|
||||
};
|
||||
|
||||
export const adminRolesService = {
|
||||
getAll,
|
||||
create,
|
||||
update,
|
||||
remove,
|
||||
assignPermission,
|
||||
removePermission,
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
// Role DTOs - Matches Backend
|
||||
export interface RoleResponseDto {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
permissions: PermissionInfo[];
|
||||
_count?: {
|
||||
users: number;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface PermissionInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
// Create/Update Role
|
||||
export interface CreateRoleDto {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateRoleDto {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// Role Permission Assignment
|
||||
export interface RolePermissionResponseDto {
|
||||
id: string;
|
||||
roleId: string;
|
||||
permissionId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { adminRolesService } from "./service";
|
||||
import {
|
||||
RoleResponseDto,
|
||||
CreateRoleDto,
|
||||
UpdateRoleDto,
|
||||
RolePermissionResponseDto,
|
||||
} from "./types";
|
||||
|
||||
export const AdminRolesQueryKeys = {
|
||||
all: ["admin-roles"] as const,
|
||||
list: () => [...AdminRolesQueryKeys.all, "list"] as const,
|
||||
};
|
||||
|
||||
export function useGetAllRoles() {
|
||||
const queryKey = AdminRolesQueryKeys.list();
|
||||
|
||||
const { data, ...rest } = useQuery<ApiResponse<RoleResponseDto[]>>({
|
||||
queryKey: queryKey,
|
||||
queryFn: adminRolesService.getAll,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useCreateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<RoleResponseDto>,
|
||||
Error,
|
||||
CreateRoleDto
|
||||
>({
|
||||
mutationFn: (roleData) => adminRolesService.create(roleData),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminRolesQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useUpdateRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<RoleResponseDto>,
|
||||
Error,
|
||||
{ roleId: string; data: UpdateRoleDto }
|
||||
>({
|
||||
mutationFn: ({ roleId, data }) => adminRolesService.update(roleId, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminRolesQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useDeleteRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<null>,
|
||||
Error,
|
||||
{ roleId: string }
|
||||
>({
|
||||
mutationFn: ({ roleId }) => adminRolesService.remove(roleId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminRolesQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useAssignPermission() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<RolePermissionResponseDto>,
|
||||
Error,
|
||||
{ roleId: string; permissionId: string }
|
||||
>({
|
||||
mutationFn: ({ roleId, permissionId }) =>
|
||||
adminRolesService.assignPermission(roleId, permissionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminRolesQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRemovePermission() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<null>,
|
||||
Error,
|
||||
{ roleId: string; permissionId: string }
|
||||
>({
|
||||
mutationFn: ({ roleId, permissionId }) =>
|
||||
adminRolesService.removePermission(roleId, permissionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminRolesQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { UserResponseDto } from "@/types/user";
|
||||
import {
|
||||
UsersQueryParams,
|
||||
PaginatedUsersResponse,
|
||||
UserRoleResponseDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Admin Users Service - Example Implementation
|
||||
* Matches Backend: /api/admin/users/*
|
||||
*/
|
||||
|
||||
const getAll = (params?: UsersQueryParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedUsersResponse>>({
|
||||
url: "/admin/users",
|
||||
client: "admin",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleActive = (userId: string) => {
|
||||
return apiRequest<ApiResponse<UserResponseDto>>({
|
||||
url: `/admin/users/${userId}/toggle-active`,
|
||||
client: "admin",
|
||||
method: "put",
|
||||
});
|
||||
};
|
||||
|
||||
const assignRole = (userId: string, roleId: string) => {
|
||||
return apiRequest<ApiResponse<UserRoleResponseDto>>({
|
||||
url: `/admin/users/${userId}/roles/${roleId}`,
|
||||
client: "admin",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
const removeRole = (userId: string, roleId: string) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: `/admin/users/${userId}/roles/${roleId}`,
|
||||
client: "admin",
|
||||
method: "delete",
|
||||
});
|
||||
};
|
||||
|
||||
export const adminUsersService = {
|
||||
getAll,
|
||||
toggleActive,
|
||||
assignRole,
|
||||
removeRole,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { PaginatedData, PaginationDto } from "@/types/api-response";
|
||||
import { UserResponseDto } from "../../users/types";
|
||||
export type { UserResponseDto };
|
||||
export type { PaginationDto };
|
||||
|
||||
// User Role Assignment
|
||||
export interface UserRoleResponseDto {
|
||||
id: string;
|
||||
userId: string;
|
||||
roleId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// Pagination params
|
||||
export type UsersQueryParams = PaginationDto;
|
||||
|
||||
// Response type for paginated users
|
||||
export type PaginatedUsersResponse = PaginatedData<UserResponseDto>;
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { adminUsersService } from "./service";
|
||||
import {
|
||||
UserRoleResponseDto,
|
||||
UsersQueryParams,
|
||||
PaginatedUsersResponse,
|
||||
} from "./types";
|
||||
import { UserResponseDto } from "@/types/user";
|
||||
|
||||
export const AdminUsersQueryKeys = {
|
||||
all: ["admin-users"] as const,
|
||||
list: (params?: UsersQueryParams) =>
|
||||
[...AdminUsersQueryKeys.all, "list", params] as const,
|
||||
};
|
||||
|
||||
export function useGetAllUsers(params?: UsersQueryParams) {
|
||||
const queryKey = AdminUsersQueryKeys.list(params);
|
||||
|
||||
const { data, ...rest } = useQuery<ApiResponse<PaginatedUsersResponse>>({
|
||||
queryKey: queryKey,
|
||||
queryFn: () => adminUsersService.getAll(params),
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useToggleUserActive() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<UserResponseDto>,
|
||||
Error,
|
||||
{ userId: string }
|
||||
>({
|
||||
mutationFn: ({ userId }) => adminUsersService.toggleActive(userId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminUsersQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useAssignRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<UserRoleResponseDto>,
|
||||
Error,
|
||||
{ userId: string; roleId: string }
|
||||
>({
|
||||
mutationFn: ({ userId, roleId }) =>
|
||||
adminUsersService.assignRole(userId, roleId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminUsersQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRemoveRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<null>,
|
||||
Error,
|
||||
{ userId: string; roleId: string }
|
||||
>({
|
||||
mutationFn: ({ userId, roleId }) =>
|
||||
adminUsersService.removeRole(userId, roleId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AdminUsersQueryKeys.all });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
LoginDto,
|
||||
AuthResponse,
|
||||
RegisterDto,
|
||||
RefreshTokenDto,
|
||||
} from "@/lib/api/example/auth/types";
|
||||
|
||||
/**
|
||||
* Auth Service - Example Implementation
|
||||
* Matches Backend: /api/auth/*
|
||||
*/
|
||||
|
||||
const login = (data: LoginDto) => {
|
||||
return apiRequest<ApiResponse<AuthResponse>>({
|
||||
url: "/auth/login",
|
||||
client: "auth",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const register = (data: RegisterDto) => {
|
||||
return apiRequest<ApiResponse<AuthResponse>>({
|
||||
url: "/auth/register",
|
||||
client: "auth",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const refreshToken = (data: RefreshTokenDto) => {
|
||||
return apiRequest<ApiResponse<AuthResponse>>({
|
||||
url: "/auth/refresh",
|
||||
client: "auth",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: "/auth/logout",
|
||||
client: "auth",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
export const authService = {
|
||||
login,
|
||||
register,
|
||||
refreshToken,
|
||||
logout,
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
// Auth Request DTOs
|
||||
export interface LoginDto {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface RegisterDto {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export interface RefreshTokenDto {
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
// Auth Response DTOs
|
||||
export interface AuthResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresIn: number;
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
isActive?: boolean;
|
||||
roles: string[];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { authService } from "./service";
|
||||
import { LoginDto, RegisterDto, RefreshTokenDto, AuthResponse } from "./types";
|
||||
|
||||
export const AuthQueryKeys = {
|
||||
all: ["auth"] as const,
|
||||
session: () => [...AuthQueryKeys.all, "session"] as const,
|
||||
};
|
||||
|
||||
export function useLogin() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<AuthResponse>,
|
||||
Error,
|
||||
LoginDto
|
||||
>({
|
||||
mutationFn: (credentials: LoginDto) => authService.login(credentials),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: AuthQueryKeys.session() });
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRegister() {
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<AuthResponse>,
|
||||
Error,
|
||||
RegisterDto
|
||||
>({
|
||||
mutationFn: (userData: RegisterDto) => authService.register(userData),
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useRefreshToken() {
|
||||
const { data, ...rest } = useMutation<
|
||||
ApiResponse<AuthResponse>,
|
||||
Error,
|
||||
RefreshTokenDto
|
||||
>({
|
||||
mutationFn: (tokenData: RefreshTokenDto) =>
|
||||
authService.refreshToken(tokenData),
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
|
||||
export function useLogout() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, ...rest } = useMutation<ApiResponse<null>, Error, void>({
|
||||
mutationFn: () => authService.logout(),
|
||||
onSuccess: () => {
|
||||
queryClient.clear();
|
||||
},
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Example API Services - Barrel Export
|
||||
* Import all services and hooks from this single file
|
||||
*
|
||||
* Usage:
|
||||
* import { authService, useLogin, adminRolesService, useGetAllRoles } from '@/lib/api/example';
|
||||
*/
|
||||
|
||||
// Services
|
||||
|
||||
export { usersService } from "./users/service";
|
||||
export { adminUsersService } from "./admin/users/service";
|
||||
export { adminRolesService } from "./admin/roles/service";
|
||||
export { adminPermissionsService } from "./admin/permissions/service";
|
||||
|
||||
// Hooks - Auth
|
||||
export {
|
||||
useLogin,
|
||||
useRegister,
|
||||
useRefreshToken,
|
||||
useLogout,
|
||||
AuthQueryKeys,
|
||||
} from "./auth/use-hooks";
|
||||
|
||||
// Hooks - Users
|
||||
export { useGetMe, UsersQueryKeys } from "./users/use-hooks";
|
||||
|
||||
// Hooks - Admin Users
|
||||
export {
|
||||
useGetAllUsers,
|
||||
useToggleUserActive,
|
||||
useAssignRole,
|
||||
useRemoveRole,
|
||||
AdminUsersQueryKeys,
|
||||
} from "./admin/users/use-hooks";
|
||||
|
||||
// Hooks - Admin Roles
|
||||
export {
|
||||
useGetAllRoles,
|
||||
useCreateRole,
|
||||
useUpdateRole,
|
||||
useDeleteRole,
|
||||
useAssignPermission,
|
||||
useRemovePermission,
|
||||
AdminRolesQueryKeys,
|
||||
} from "./admin/roles/use-hooks";
|
||||
|
||||
// Hooks - Admin Permissions
|
||||
export {
|
||||
useGetAllPermissions,
|
||||
useCreatePermission,
|
||||
AdminPermissionsQueryKeys,
|
||||
} from "./admin/permissions/use-hooks";
|
||||
|
||||
// Types - Auth
|
||||
export type {
|
||||
LoginDto,
|
||||
RegisterDto,
|
||||
RefreshTokenDto,
|
||||
AuthResponse,
|
||||
} from "./auth/types";
|
||||
|
||||
// Types - Users (Common)
|
||||
export type { UserResponseDto, RoleInfo } from "./users/types";
|
||||
|
||||
// Types - Admin Users (Specific)
|
||||
export type {
|
||||
UserRoleResponseDto,
|
||||
UsersQueryParams,
|
||||
PaginatedUsersResponse,
|
||||
} from "./admin/users/types";
|
||||
|
||||
// Types - Admin Roles
|
||||
export type {
|
||||
RoleResponseDto,
|
||||
CreateRoleDto,
|
||||
UpdateRoleDto,
|
||||
RolePermissionResponseDto,
|
||||
PermissionInfo,
|
||||
} from "./admin/roles/types";
|
||||
|
||||
// Types - Admin Permissions
|
||||
export type {
|
||||
PermissionResponseDto,
|
||||
CreatePermissionDto,
|
||||
} from "./admin/permissions/types";
|
||||
@@ -0,0 +1,20 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { UserResponseDto } from "./types";
|
||||
|
||||
/**
|
||||
* Users Service - Example Implementation
|
||||
* Matches Backend: /api/users/*
|
||||
*/
|
||||
|
||||
const getMe = () => {
|
||||
return apiRequest<ApiResponse<UserResponseDto>>({
|
||||
url: "/users/me",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
export const usersService = {
|
||||
getMe,
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
// User Response DTO - Matches Backend UserResponseDto
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username?: string;
|
||||
isActive: boolean;
|
||||
avatar?: string | null;
|
||||
roles: RoleInfo[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RoleInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import { usersService } from "./service";
|
||||
import { UserResponseDto } from "./types";
|
||||
|
||||
export const UsersQueryKeys = {
|
||||
all: ["users"] as const,
|
||||
me: () => [...UsersQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
export function useGetMe() {
|
||||
const queryKey = UsersQueryKeys.me();
|
||||
|
||||
const { data, ...rest } = useQuery<ApiResponse<UserResponseDto>>({
|
||||
queryKey: queryKey,
|
||||
queryFn: usersService.getMe,
|
||||
});
|
||||
|
||||
return { data: data?.data, ...rest };
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// =========================================================
|
||||
// Barrel Export — All API services, hooks, types & query keys
|
||||
// =========================================================
|
||||
|
||||
// ── Matches ──────────────────────────────────────────────
|
||||
export { matchesService } from "@/lib/api/matches/service";
|
||||
export {
|
||||
useListMatches,
|
||||
useMatchDetails,
|
||||
useQueryMatches,
|
||||
useActiveLeagues,
|
||||
MatchesQueryKeys,
|
||||
} from "@/lib/api/matches/use-hooks";
|
||||
export type {
|
||||
SportType,
|
||||
MatchStatus,
|
||||
MatchQueryDto,
|
||||
MatchListParams,
|
||||
MatchResponseDto,
|
||||
ActiveLeagueDto,
|
||||
LeagueWithMatchesDto,
|
||||
} from "@/lib/api/matches/types";
|
||||
|
||||
// ── Predictions ──────────────────────────────────────────
|
||||
export { predictionsService } from "@/lib/api/predictions/service";
|
||||
export {
|
||||
usePrediction,
|
||||
useUpcomingPredictions,
|
||||
useValueBets,
|
||||
usePredictionHistory,
|
||||
useAIHealth,
|
||||
useGenerateSmartCoupon,
|
||||
PredictionsQueryKeys,
|
||||
} from "@/lib/api/predictions/use-hooks";
|
||||
export type {
|
||||
MatchPredictionDto,
|
||||
ValueBetDto,
|
||||
UpcomingPredictionsDto,
|
||||
PredictionHistoryResponseDto,
|
||||
AIHealthDto,
|
||||
SmartCouponRequestDto,
|
||||
CouponStrategy,
|
||||
MatchPickDto,
|
||||
MatchBetSummaryItemDto,
|
||||
MatchBetAdviceDto,
|
||||
ScorePredictionDto,
|
||||
RiskDto,
|
||||
DataQualityDto,
|
||||
EngineBreakdownDto,
|
||||
BetGrade,
|
||||
} from "@/lib/api/predictions/types";
|
||||
|
||||
// ── Coupons ──────────────────────────────────────────────
|
||||
export { couponsService } from "@/lib/api/coupons/service";
|
||||
export {
|
||||
useAnalyzeMatch,
|
||||
useCreateCoupon,
|
||||
useDailyBanko,
|
||||
useCouponHistory,
|
||||
useUserBettingStats,
|
||||
useSuggestCoupon,
|
||||
CouponsQueryKeys,
|
||||
} from "@/lib/api/coupons/use-hooks";
|
||||
export type {
|
||||
CreateCouponDto,
|
||||
CouponItemDto,
|
||||
SuggestCouponDto,
|
||||
CouponResponseDto,
|
||||
UserBettingStatsDto,
|
||||
} from "@/lib/api/coupons/types";
|
||||
|
||||
// ── Leagues ──────────────────────────────────────────────
|
||||
export { leaguesService } from "@/lib/api/leagues/service";
|
||||
export {
|
||||
useLeagues,
|
||||
useLeagueById,
|
||||
useCountries,
|
||||
useCountryById,
|
||||
useTeamById,
|
||||
useTeamMatches,
|
||||
useHeadToHead,
|
||||
useSearchTeams,
|
||||
LeaguesQueryKeys,
|
||||
} from "@/lib/api/leagues/use-hooks";
|
||||
export type {
|
||||
LeagueDto,
|
||||
CountryDto,
|
||||
TeamDto,
|
||||
HeadToHeadDto,
|
||||
TeamSearchParams,
|
||||
HeadToHeadParams,
|
||||
} from "@/lib/api/leagues/types";
|
||||
|
||||
// ── Analysis ─────────────────────────────────────────────
|
||||
export { analysisService } from "@/lib/api/analysis/service";
|
||||
export {
|
||||
useAnalyzeMatches,
|
||||
useAnalysisHistory,
|
||||
AnalysisQueryKeys,
|
||||
} from "@/lib/api/analysis/use-hooks";
|
||||
export type {
|
||||
AnalyzeMatchesDto,
|
||||
AnalysisResultDto,
|
||||
} from "@/lib/api/analysis/types";
|
||||
|
||||
// ── Admin ────────────────────────────────────────────────
|
||||
export { adminService } from "@/lib/api/admin/service";
|
||||
export {
|
||||
useAdminAnalytics,
|
||||
useAdminSettings,
|
||||
useUpdateSetting,
|
||||
useAdminUsageLimits,
|
||||
useResetAllUsageLimits,
|
||||
useAdminUsers,
|
||||
useAdminUserById,
|
||||
useDeleteUser,
|
||||
useUpdateUserRole,
|
||||
useUpdateUserSubscription,
|
||||
useToggleUserActive,
|
||||
AdminQueryKeys,
|
||||
} from "@/lib/api/admin/use-hooks";
|
||||
export type {
|
||||
AdminUserDto,
|
||||
SettingDto,
|
||||
UsageLimitDto,
|
||||
AnalyticsOverviewDto,
|
||||
AdminPaginationParams,
|
||||
} from "@/lib/api/admin/types";
|
||||
|
||||
// ── Spor Toto ────────────────────────────────────────────
|
||||
export { sporTotoService } from "@/lib/api/spor-toto/service";
|
||||
export {
|
||||
useBulletins,
|
||||
useBulletinById,
|
||||
useBulletinStats,
|
||||
useRolloverHistory,
|
||||
useSyncBulletin,
|
||||
useGenerateColumns,
|
||||
useEvaluateColumns,
|
||||
useGeneratePrediction,
|
||||
SporTotoQueryKeys,
|
||||
} from "@/lib/api/spor-toto/use-hooks";
|
||||
|
||||
// ── Users ────────────────────────────────────────────────
|
||||
export { usersService } from "@/lib/api/users/service";
|
||||
export {
|
||||
useUpdateProfile,
|
||||
useChangePassword,
|
||||
UsersQueryKeys,
|
||||
} from "@/lib/api/users/use-hooks";
|
||||
@@ -0,0 +1,97 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
import type {
|
||||
LeagueDto,
|
||||
CountryDto,
|
||||
TeamDto,
|
||||
HeadToHeadDto,
|
||||
LeagueQueryParams,
|
||||
TeamSearchParams,
|
||||
HeadToHeadParams,
|
||||
TeamMatchesParams,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Leagues Service
|
||||
* Backend: /api/leagues/*
|
||||
*/
|
||||
|
||||
const getLeagues = (params?: LeagueQueryParams) => {
|
||||
return apiRequest<ApiResponse<LeagueDto[]>>({
|
||||
url: "/leagues",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const getLeagueById = (id: string) => {
|
||||
return apiRequest<ApiResponse<LeagueDto>>({
|
||||
url: `/leagues/${id}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getCountries = () => {
|
||||
return apiRequest<ApiResponse<CountryDto[]>>({
|
||||
url: "/leagues/countries",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getCountryById = (id: string) => {
|
||||
return apiRequest<ApiResponse<CountryDto>>({
|
||||
url: `/leagues/countries/${id}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getTeamById = (id: string) => {
|
||||
return apiRequest<ApiResponse<TeamDto>>({
|
||||
url: `/leagues/teams/${id}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getTeamMatches = (id: string, params?: TeamMatchesParams) => {
|
||||
return apiRequest<ApiResponse<MatchResponseDto[]>>({
|
||||
url: `/leagues/teams/${id}/matches`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const getHeadToHead = (params: HeadToHeadParams) => {
|
||||
return apiRequest<ApiResponse<HeadToHeadDto>>({
|
||||
url: "/leagues/teams/h2h",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const searchTeams = (params: TeamSearchParams) => {
|
||||
return apiRequest<ApiResponse<TeamDto[]>>({
|
||||
url: "/leagues/teams/search",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
export const leaguesService = {
|
||||
getLeagues,
|
||||
getLeagueById,
|
||||
getCountries,
|
||||
getCountryById,
|
||||
getTeamById,
|
||||
getTeamMatches,
|
||||
getHeadToHead,
|
||||
searchTeams,
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
import type { MatchResponseDto } from "@/lib/api/matches/types";
|
||||
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface LeagueQueryParams {
|
||||
sport?: SportType;
|
||||
}
|
||||
|
||||
export interface TeamSearchParams {
|
||||
q: string;
|
||||
sport?: SportType;
|
||||
}
|
||||
|
||||
export interface HeadToHeadParams {
|
||||
team1: string;
|
||||
team2: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface TeamMatchesParams {
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface LeagueDto {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
country?: CountryDto;
|
||||
sport?: SportType;
|
||||
season?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CountryDto {
|
||||
id: string;
|
||||
name: string;
|
||||
flag?: string;
|
||||
leagues?: LeagueDto[];
|
||||
}
|
||||
|
||||
export interface TeamDto {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
country?: string;
|
||||
sport?: SportType;
|
||||
}
|
||||
|
||||
export interface HeadToHeadDto {
|
||||
matches: MatchResponseDto[];
|
||||
team1Wins: number;
|
||||
team2Wins: number;
|
||||
draws: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { leaguesService } from "./service";
|
||||
import type {
|
||||
LeagueQueryParams,
|
||||
TeamSearchParams,
|
||||
HeadToHeadParams,
|
||||
TeamMatchesParams,
|
||||
} from "./types";
|
||||
|
||||
export const LeaguesQueryKeys = {
|
||||
all: ["leagues"] as const,
|
||||
list: (params?: LeagueQueryParams) =>
|
||||
[...LeaguesQueryKeys.all, "list", params] as const,
|
||||
detail: (id: string) => [...LeaguesQueryKeys.all, "detail", id] as const,
|
||||
countries: () => [...LeaguesQueryKeys.all, "countries"] as const,
|
||||
country: (id: string) => [...LeaguesQueryKeys.all, "country", id] as const,
|
||||
team: (id: string) => [...LeaguesQueryKeys.all, "team", id] as const,
|
||||
teamMatches: (id: string, params?: TeamMatchesParams) =>
|
||||
[...LeaguesQueryKeys.all, "teamMatches", id, params] as const,
|
||||
headToHead: (params: HeadToHeadParams) =>
|
||||
[...LeaguesQueryKeys.all, "h2h", params] as const,
|
||||
searchTeams: (params: TeamSearchParams) =>
|
||||
[...LeaguesQueryKeys.all, "searchTeams", params] as const,
|
||||
};
|
||||
|
||||
export const useLeagues = (params?: LeagueQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.list(params),
|
||||
queryFn: () => leaguesService.getLeagues(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useLeagueById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.detail(id),
|
||||
queryFn: () => leaguesService.getLeagueById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCountries = () => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.countries(),
|
||||
queryFn: () => leaguesService.getCountries(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useCountryById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.country(id),
|
||||
queryFn: () => leaguesService.getCountryById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTeamById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.team(id),
|
||||
queryFn: () => leaguesService.getTeamById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useTeamMatches = (id: string, params?: TeamMatchesParams) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.teamMatches(id, params),
|
||||
queryFn: () => leaguesService.getTeamMatches(id, params),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useHeadToHead = (params: HeadToHeadParams) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.headToHead(params),
|
||||
queryFn: () => leaguesService.getHeadToHead(params),
|
||||
enabled: !!params.team1 && !!params.team2,
|
||||
});
|
||||
};
|
||||
|
||||
export const useSearchTeams = (params: TeamSearchParams) => {
|
||||
return useQuery({
|
||||
queryKey: LeaguesQueryKeys.searchTeams(params),
|
||||
queryFn: () => leaguesService.searchTeams(params),
|
||||
enabled: params.q.length >= 2,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse, PaginatedData } from "@/types/api-response";
|
||||
import {
|
||||
MatchResponseDto,
|
||||
MatchListParams,
|
||||
MatchQueryDto,
|
||||
ActiveLeagueDto,
|
||||
LeagueWithMatchesDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Matches Service
|
||||
* Backend: /api/matches/*
|
||||
*/
|
||||
|
||||
const listMatches = (params?: MatchListParams) => {
|
||||
return apiRequest<ApiResponse<PaginatedData<MatchResponseDto>>>({
|
||||
url: "/matches",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
};
|
||||
|
||||
const getMatchDetails = (id: string) => {
|
||||
return apiRequest<ApiResponse<MatchResponseDto>>({
|
||||
url: `/matches/${id}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const queryMatches = (queryDto: MatchQueryDto) => {
|
||||
return apiRequest<ApiResponse<LeagueWithMatchesDto[]>>({
|
||||
url: "/matches/query",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: queryDto,
|
||||
});
|
||||
};
|
||||
|
||||
const getActiveLeagues = (sport?: string) => {
|
||||
return apiRequest<ApiResponse<ActiveLeagueDto[]>>({
|
||||
url: "/matches/leagues/active",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params: sport ? { sport } : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
export const matchesService = {
|
||||
listMatches,
|
||||
getMatchDetails,
|
||||
queryMatches,
|
||||
getActiveLeagues,
|
||||
};
|
||||
@@ -0,0 +1,118 @@
|
||||
// ========================
|
||||
// Enums & Constants
|
||||
// ========================
|
||||
|
||||
export type SportType = "football" | "basketball";
|
||||
|
||||
export type MatchStatus =
|
||||
| "LIVE"
|
||||
| "Finished"
|
||||
| "Not Started"
|
||||
| "UPCOMING"
|
||||
| "NOT_STARTED"
|
||||
| string;
|
||||
|
||||
// ========================
|
||||
// Query DTOs
|
||||
// ========================
|
||||
|
||||
export interface TeamFilterDto {
|
||||
name?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface OddFilterDto {
|
||||
market: string;
|
||||
minOdd?: number;
|
||||
maxOdd?: number;
|
||||
}
|
||||
|
||||
export interface DateRangeDto {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface MatchQueryDto {
|
||||
sport: SportType;
|
||||
limit?: number;
|
||||
leagueId?: string;
|
||||
status?: MatchStatus;
|
||||
date?: string; // YYYY-MM-DD
|
||||
team?: TeamFilterDto;
|
||||
odds?: OddFilterDto[];
|
||||
dateRange?: DateRangeDto;
|
||||
}
|
||||
|
||||
export interface MatchListParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
sport?: SportType;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface MatchResponseDto {
|
||||
id: string;
|
||||
matchName: string;
|
||||
matchSlug?: string;
|
||||
mstUtc: number | string; // Timestamp
|
||||
date?: string | Date;
|
||||
status: MatchStatus;
|
||||
state?: string;
|
||||
|
||||
// Scores
|
||||
scoreHome?: number;
|
||||
scoreAway?: number;
|
||||
score?: { home: number; away: number };
|
||||
htScoreHome?: number;
|
||||
htScoreAway?: number;
|
||||
|
||||
// Teams
|
||||
homeTeamName: string;
|
||||
homeTeamLogo?: string;
|
||||
awayTeamName: string;
|
||||
awayTeamLogo?: string;
|
||||
|
||||
// League & Country
|
||||
leagueName?: string;
|
||||
countryName?: string;
|
||||
|
||||
// Odds (dynamic key-value)
|
||||
odds?: Record<string, Record<string, { odd: string }>>;
|
||||
|
||||
// Nested Objects (from Backend include)
|
||||
homeTeam?: { name: string; logo?: string; [key: string]: unknown };
|
||||
awayTeam?: { name: string; logo?: string; [key: string]: unknown };
|
||||
league?: {
|
||||
name: string;
|
||||
country?: { name: string; flag?: string };
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ActiveLeagueDto {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
countryName?: string;
|
||||
countryFlag?: string;
|
||||
matchCount: number;
|
||||
liveCount: number;
|
||||
}
|
||||
|
||||
export interface LeagueWithMatchesDto {
|
||||
id: string;
|
||||
name: string;
|
||||
code?: string;
|
||||
country: {
|
||||
id: string;
|
||||
name: string;
|
||||
flagUrl?: string; // Backend uses flagUrl
|
||||
};
|
||||
sport: SportType;
|
||||
matches: MatchResponseDto[];
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { matchesService } from "./service";
|
||||
import type { MatchListParams, MatchQueryDto } from "./types";
|
||||
|
||||
export const MatchesQueryKeys = {
|
||||
all: ["matches"] as const,
|
||||
list: (params?: MatchListParams) =>
|
||||
[...MatchesQueryKeys.all, "list", params] as const,
|
||||
detail: (id: string) => [...MatchesQueryKeys.all, "detail", id] as const,
|
||||
activeLeagues: (sport?: string) =>
|
||||
[...MatchesQueryKeys.all, "activeLeagues", sport] as const,
|
||||
};
|
||||
|
||||
export const useListMatches = (params?: MatchListParams) => {
|
||||
return useQuery({
|
||||
queryKey: MatchesQueryKeys.list(params),
|
||||
queryFn: () => matchesService.listMatches(params),
|
||||
});
|
||||
};
|
||||
|
||||
export const useMatchDetails = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: MatchesQueryKeys.detail(id),
|
||||
queryFn: () => matchesService.getMatchDetails(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useQueryMatches = () => {
|
||||
return useMutation({
|
||||
mutationFn: (queryDto: MatchQueryDto) =>
|
||||
matchesService.queryMatches(queryDto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useActiveLeagues = (sport?: string) => {
|
||||
return useQuery({
|
||||
queryKey: MatchesQueryKeys.activeLeagues(sport),
|
||||
queryFn: () => matchesService.getActiveLeagues(sport),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
import {
|
||||
MatchPredictionDto,
|
||||
UpcomingPredictionsDto,
|
||||
ValueBetDto,
|
||||
PredictionHistoryResponseDto,
|
||||
AIHealthDto,
|
||||
SmartCouponRequestDto,
|
||||
SmartCouponResponseDto,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Predictions Service
|
||||
* Backend: /api/predictions/*
|
||||
*/
|
||||
|
||||
const getPrediction = (matchId: string) => {
|
||||
return apiRequest<ApiResponse<MatchPredictionDto>>({
|
||||
url: `/predictions/${matchId}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const generatePrediction = (body: { matchId: string; sport?: SportType }) => {
|
||||
return apiRequest<ApiResponse<MatchPredictionDto>>({
|
||||
url: "/predictions/generate",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
};
|
||||
|
||||
const getUpcoming = () => {
|
||||
return apiRequest<ApiResponse<UpcomingPredictionsDto>>({
|
||||
url: "/predictions/upcoming",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getValueBets = () => {
|
||||
return apiRequest<ApiResponse<ValueBetDto[]>>({
|
||||
url: "/predictions/value-bets",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getHistory = () => {
|
||||
return apiRequest<ApiResponse<PredictionHistoryResponseDto>>({
|
||||
url: "/predictions/history",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const checkHealth = () => {
|
||||
return apiRequest<ApiResponse<AIHealthDto>>({
|
||||
url: "/predictions/health",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const generateSmartCoupon = (body: SmartCouponRequestDto) => {
|
||||
return apiRequest<ApiResponse<SmartCouponResponseDto>>({
|
||||
url: "/predictions/smart-coupon",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: body,
|
||||
});
|
||||
};
|
||||
|
||||
export const predictionsService = {
|
||||
getPrediction,
|
||||
generatePrediction,
|
||||
getUpcoming,
|
||||
getValueBets,
|
||||
getHistory,
|
||||
checkHealth,
|
||||
generateSmartCoupon,
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
|
||||
// ========================
|
||||
// Sub-DTOs for MatchPredictionDto
|
||||
// ========================
|
||||
|
||||
export interface MatchInfoDto {
|
||||
match_id: string;
|
||||
match_name: string;
|
||||
home_team: string;
|
||||
away_team: string;
|
||||
league: string;
|
||||
match_date_ms: number;
|
||||
league_id?: string | null;
|
||||
is_top_league?: boolean;
|
||||
sport?: SportType;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface DataQualityDto {
|
||||
label: "HIGH" | "MEDIUM" | "LOW";
|
||||
score: number;
|
||||
home_lineup_count: number;
|
||||
away_lineup_count: number;
|
||||
lineup_source: string;
|
||||
flags: string[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ConfidenceIntervalDto {
|
||||
lower: number;
|
||||
upper: number;
|
||||
width: number;
|
||||
band: "HIGH" | "MEDIUM" | "LOW";
|
||||
threshold_met: boolean;
|
||||
}
|
||||
|
||||
export interface RiskDto {
|
||||
level: "LOW" | "MEDIUM" | "HIGH" | "EXTREME";
|
||||
score: number;
|
||||
is_surprise_risk: boolean;
|
||||
surprise_type: string | null;
|
||||
surprise_score?: number;
|
||||
surprise_comment?: string | null;
|
||||
surprise_reasons?: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface EngineBreakdownDto {
|
||||
team: number;
|
||||
player: number;
|
||||
odds: number;
|
||||
referee: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export type BetGrade = "A" | "B" | "C" | "PASS";
|
||||
export type SignalTier = "CORE" | "VALUE" | "LEAN" | "LONGSHOT" | "PASS";
|
||||
|
||||
export interface MatchPickDto {
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
min_required_confidence: number;
|
||||
edge: number;
|
||||
ev_edge: number;
|
||||
implied_prob: number;
|
||||
play_score: number;
|
||||
playable: boolean;
|
||||
bet_grade: BetGrade;
|
||||
stake_units: number;
|
||||
decision_reasons: string[];
|
||||
confidence_interval?: ConfidenceIntervalDto;
|
||||
signal_tier?: SignalTier;
|
||||
}
|
||||
|
||||
export interface MatchBetAdviceDto {
|
||||
playable: boolean;
|
||||
suggested_stake_units: number;
|
||||
reason: string;
|
||||
confidence_band?: "HIGH" | "MEDIUM" | "LOW";
|
||||
min_confidence_for_play?: number;
|
||||
signal_tier?: SignalTier;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MatchBetSummaryItemDto {
|
||||
market: string;
|
||||
pick: string;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
bet_grade: BetGrade;
|
||||
playable: boolean;
|
||||
stake_units: number;
|
||||
play_score: number;
|
||||
ev_edge: number;
|
||||
implied_prob: number;
|
||||
odds: number;
|
||||
reasons: string[];
|
||||
confidence_interval?: ConfidenceIntervalDto;
|
||||
signal_tier?: SignalTier;
|
||||
}
|
||||
|
||||
export interface AggressivePickDto {
|
||||
market: string;
|
||||
pick: string;
|
||||
probability: number;
|
||||
confidence: number;
|
||||
odds: number | null;
|
||||
confidence_interval?: ConfidenceIntervalDto;
|
||||
}
|
||||
|
||||
export interface ScenarioTop5ItemDto {
|
||||
score: string;
|
||||
prob: number;
|
||||
}
|
||||
|
||||
export interface ScorePredictionDto {
|
||||
ft: string;
|
||||
ht: string;
|
||||
xg_home: number;
|
||||
xg_away: number;
|
||||
xg_total: number;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Main Prediction DTOs
|
||||
// ========================
|
||||
|
||||
export interface MarketBoardEntryDto {
|
||||
pick?: string;
|
||||
confidence?: number;
|
||||
confidence_band?: "HIGH" | "MEDIUM" | "LOW";
|
||||
confidence_interval?: ConfidenceIntervalDto;
|
||||
playable?: boolean;
|
||||
probs?: Record<string, number>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface MatchPredictionDto {
|
||||
model_version: string;
|
||||
match_info: MatchInfoDto;
|
||||
data_quality: DataQualityDto;
|
||||
risk: RiskDto;
|
||||
engine_breakdown: EngineBreakdownDto;
|
||||
main_pick: MatchPickDto | null;
|
||||
bet_advice: MatchBetAdviceDto;
|
||||
bet_summary: MatchBetSummaryItemDto[];
|
||||
supporting_picks: MatchPickDto[];
|
||||
aggressive_pick: AggressivePickDto | null;
|
||||
scenario_top5: ScenarioTop5ItemDto[];
|
||||
score_prediction: ScorePredictionDto;
|
||||
market_board: Record<string, MarketBoardEntryDto>;
|
||||
reasoning_factors: string[];
|
||||
ai_commentary?: string | null;
|
||||
}
|
||||
|
||||
export interface ValueBetDto {
|
||||
matchId: string;
|
||||
matchName: string;
|
||||
betType: string;
|
||||
prediction: string;
|
||||
confidence: number;
|
||||
odd: number;
|
||||
expectedValue: number;
|
||||
}
|
||||
|
||||
export interface UpcomingPredictionsDto {
|
||||
count: number;
|
||||
matches: MatchPredictionDto[];
|
||||
modelVersion: string;
|
||||
}
|
||||
|
||||
export interface PredictionHistoryStatsDto {
|
||||
totalPredictions: number;
|
||||
correctPredictions: number;
|
||||
accuracy: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PredictionHistoryResponseDto {
|
||||
stats: PredictionHistoryStatsDto;
|
||||
history: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
export interface AIHealthDto {
|
||||
status: string;
|
||||
modelLoaded: boolean;
|
||||
predictionServiceReady: boolean;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Smart Coupon Request DTO
|
||||
// ========================
|
||||
|
||||
export type CouponStrategy =
|
||||
| "SAFE"
|
||||
| "BALANCED"
|
||||
| "AGGRESSIVE"
|
||||
| "VALUE"
|
||||
| "MIRACLE";
|
||||
|
||||
export interface SmartCouponRequestDto {
|
||||
matchIds: string[];
|
||||
strategy?: CouponStrategy;
|
||||
maxMatches?: number;
|
||||
minConfidence?: number;
|
||||
}
|
||||
|
||||
export interface SmartCouponResponseDto {
|
||||
coupon: {
|
||||
items: Array<{
|
||||
matchId: string;
|
||||
matchName: string;
|
||||
market: string;
|
||||
pick: string;
|
||||
odd: number;
|
||||
confidence: number;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
totalOdd: number;
|
||||
strategy: CouponStrategy;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
import { predictionsService } from "./service";
|
||||
import type { SmartCouponRequestDto } from "./types";
|
||||
|
||||
export const PredictionsQueryKeys = {
|
||||
all: ["predictions"] as const,
|
||||
detail: (matchId: string) =>
|
||||
[...PredictionsQueryKeys.all, "detail", matchId] as const,
|
||||
upcoming: () => [...PredictionsQueryKeys.all, "upcoming"] as const,
|
||||
valueBets: () => [...PredictionsQueryKeys.all, "valueBets"] as const,
|
||||
history: () => [...PredictionsQueryKeys.all, "history"] as const,
|
||||
health: () => [...PredictionsQueryKeys.all, "health"] as const,
|
||||
};
|
||||
|
||||
export const usePrediction = (matchId: string) => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.detail(matchId),
|
||||
queryFn: () => predictionsService.getPrediction(matchId),
|
||||
enabled: !!matchId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGeneratePrediction = () => {
|
||||
return useMutation({
|
||||
mutationFn: (body: { matchId: string; sport?: SportType }) =>
|
||||
predictionsService.generatePrediction(body),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpcomingPredictions = () => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.upcoming(),
|
||||
queryFn: () => predictionsService.getUpcoming(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useValueBets = () => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.valueBets(),
|
||||
queryFn: () => predictionsService.getValueBets(),
|
||||
});
|
||||
};
|
||||
|
||||
export const usePredictionHistory = () => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.history(),
|
||||
queryFn: () => predictionsService.getHistory(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useAIHealth = () => {
|
||||
return useQuery({
|
||||
queryKey: PredictionsQueryKeys.health(),
|
||||
queryFn: () => predictionsService.checkHealth(),
|
||||
refetchInterval: 30_000, // Auto-refresh every 30s
|
||||
});
|
||||
};
|
||||
|
||||
export const useGenerateSmartCoupon = () => {
|
||||
return useMutation({
|
||||
mutationFn: (body: SmartCouponRequestDto) =>
|
||||
predictionsService.generateSmartCoupon(body),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
|
||||
/**
|
||||
* Spor Toto Service
|
||||
* Backend: /api/spor-toto/*
|
||||
*/
|
||||
|
||||
// ========================
|
||||
// Request DTOs
|
||||
// ========================
|
||||
|
||||
export interface CreateBulletinDto {
|
||||
gameCycleNo: number;
|
||||
drawDate: string;
|
||||
matches: {
|
||||
matchId: string;
|
||||
homeTeam: string;
|
||||
awayTeam: string;
|
||||
matchDate: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface UpdateResultsDto {
|
||||
results: {
|
||||
matchIndex: number;
|
||||
homeScore: number;
|
||||
awayScore: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface GenerateColumnsDto {
|
||||
selections: { matchIndex: number; picks: ("1" | "X" | "2")[] }[];
|
||||
strategy?: "FULL" | "REDUCED";
|
||||
maxColumns?: number;
|
||||
}
|
||||
|
||||
export interface EvaluateColumnsDto {
|
||||
bulletinId: string;
|
||||
columns: string[];
|
||||
}
|
||||
|
||||
export interface GenerateSporTotoPredictionDto {
|
||||
bulletinId: string;
|
||||
strategy?: "CONSERVATIVE" | "BALANCED" | "AGGRESSIVE" | "FORMULA_6PCT";
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Response DTOs
|
||||
// ========================
|
||||
|
||||
export interface SporTotoMatchDto {
|
||||
id: string;
|
||||
matchIndex: number;
|
||||
homeTeam: string;
|
||||
awayTeam: string;
|
||||
matchDate: string;
|
||||
result?: string;
|
||||
homeScore?: number;
|
||||
awayScore?: number;
|
||||
prediction?: {
|
||||
pick: string;
|
||||
confidence: number;
|
||||
odds: Record<string, number>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SporTotoBulletinDto {
|
||||
id: string;
|
||||
gameCycleNo: number;
|
||||
drawDate: string;
|
||||
status: string;
|
||||
matches: SporTotoMatchDto[];
|
||||
totalPool?: number;
|
||||
dividends?: Record<string, number>;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface SporTotoStatsDto {
|
||||
poolDistribution: Record<string, number>;
|
||||
expectedValue: Record<string, number>;
|
||||
rolloverAmount: number;
|
||||
consecutiveRollovers: number;
|
||||
}
|
||||
|
||||
export interface ColumnGenerationResultDto {
|
||||
columns: string[];
|
||||
totalCost: number;
|
||||
strategy: string;
|
||||
}
|
||||
|
||||
export interface ColumnEvaluationResultDto {
|
||||
results: { column: string; correct: number }[];
|
||||
summary: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface SporTotoPredictionResultDto {
|
||||
bulletin: SporTotoBulletinDto;
|
||||
matchAnalysis: Record<string, unknown>;
|
||||
systemCoupon: {
|
||||
columns: string[];
|
||||
cost: number;
|
||||
};
|
||||
evReport: {
|
||||
expectedValue: number;
|
||||
playRecommendation: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RolloverHistoryItemDto {
|
||||
bulletinId: string;
|
||||
gameCycleNo: number;
|
||||
rolloverAmount: number;
|
||||
drawDate: string;
|
||||
}
|
||||
|
||||
// ========================
|
||||
// Service Methods
|
||||
// ========================
|
||||
|
||||
const syncFromApi = () => {
|
||||
return apiRequest<ApiResponse<unknown>>({
|
||||
url: "/spor-toto/sync",
|
||||
client: "core",
|
||||
method: "post",
|
||||
});
|
||||
};
|
||||
|
||||
const listBulletins = (status?: string, limit?: number) => {
|
||||
return apiRequest<ApiResponse<SporTotoBulletinDto[]>>({
|
||||
url: "/spor-toto/bulletins",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params: { status, limit },
|
||||
});
|
||||
};
|
||||
|
||||
const getBulletinById = (id: string) => {
|
||||
return apiRequest<ApiResponse<SporTotoBulletinDto>>({
|
||||
url: `/spor-toto/bulletins/${id}`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const createBulletin = (dto: CreateBulletinDto) => {
|
||||
return apiRequest<ApiResponse<SporTotoBulletinDto>>({
|
||||
url: "/spor-toto/bulletins",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const updateResults = (id: string, dto: UpdateResultsDto) => {
|
||||
return apiRequest<ApiResponse<SporTotoBulletinDto>>({
|
||||
url: `/spor-toto/bulletins/${id}/results`,
|
||||
client: "core",
|
||||
method: "patch",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const getBulletinStats = (id: string) => {
|
||||
return apiRequest<ApiResponse<SporTotoStatsDto>>({
|
||||
url: `/spor-toto/bulletins/${id}/stats`,
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const getRolloverHistory = (limit?: number) => {
|
||||
return apiRequest<ApiResponse<RolloverHistoryItemDto[]>>({
|
||||
url: "/spor-toto/history",
|
||||
client: "core",
|
||||
method: "get",
|
||||
params: { limit },
|
||||
});
|
||||
};
|
||||
|
||||
const generateColumns = (dto: GenerateColumnsDto) => {
|
||||
return apiRequest<ApiResponse<ColumnGenerationResultDto>>({
|
||||
url: "/spor-toto/columns/generate",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const evaluateColumns = (dto: EvaluateColumnsDto) => {
|
||||
return apiRequest<ApiResponse<ColumnEvaluationResultDto>>({
|
||||
url: "/spor-toto/columns/evaluate",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const generatePrediction = (dto: GenerateSporTotoPredictionDto) => {
|
||||
return apiRequest<ApiResponse<SporTotoPredictionResultDto>>({
|
||||
url: "/spor-toto/predict",
|
||||
client: "core",
|
||||
method: "post",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
export const sporTotoService = {
|
||||
syncFromApi,
|
||||
listBulletins,
|
||||
getBulletinById,
|
||||
createBulletin,
|
||||
updateResults,
|
||||
getBulletinStats,
|
||||
getRolloverHistory,
|
||||
generateColumns,
|
||||
evaluateColumns,
|
||||
generatePrediction,
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
// Re-export types from service for barrel export convenience
|
||||
export type {
|
||||
CreateBulletinDto,
|
||||
UpdateResultsDto,
|
||||
GenerateColumnsDto,
|
||||
EvaluateColumnsDto,
|
||||
GenerateSporTotoPredictionDto,
|
||||
SporTotoMatchDto,
|
||||
SporTotoBulletinDto,
|
||||
SporTotoStatsDto,
|
||||
ColumnGenerationResultDto,
|
||||
ColumnEvaluationResultDto,
|
||||
SporTotoPredictionResultDto,
|
||||
RolloverHistoryItemDto,
|
||||
} from "./service";
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { sporTotoService } from "./service";
|
||||
import type {
|
||||
GenerateColumnsDto,
|
||||
EvaluateColumnsDto,
|
||||
GenerateSporTotoPredictionDto,
|
||||
} from "./service";
|
||||
|
||||
export const SporTotoQueryKeys = {
|
||||
all: ["spor-toto"] as const,
|
||||
bulletins: (params?: { status?: string; limit?: number }) =>
|
||||
[...SporTotoQueryKeys.all, "bulletins", params] as const,
|
||||
bulletin: (id: string) => [...SporTotoQueryKeys.all, "bulletin", id] as const,
|
||||
bulletinStats: (id: string) =>
|
||||
[...SporTotoQueryKeys.all, "bulletin-stats", id] as const,
|
||||
rolloverHistory: (limit?: number) =>
|
||||
[...SporTotoQueryKeys.all, "rollover-history", limit] as const,
|
||||
};
|
||||
|
||||
export const useBulletins = (status?: string, limit?: number) => {
|
||||
return useQuery({
|
||||
queryKey: SporTotoQueryKeys.bulletins({ status, limit }),
|
||||
queryFn: () => sporTotoService.listBulletins(status, limit),
|
||||
});
|
||||
};
|
||||
|
||||
export const useBulletinById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: SporTotoQueryKeys.bulletin(id),
|
||||
queryFn: () => sporTotoService.getBulletinById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useBulletinStats = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: SporTotoQueryKeys.bulletinStats(id),
|
||||
queryFn: () => sporTotoService.getBulletinStats(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useRolloverHistory = (limit?: number) => {
|
||||
return useQuery({
|
||||
queryKey: SporTotoQueryKeys.rolloverHistory(limit),
|
||||
queryFn: () => sporTotoService.getRolloverHistory(limit),
|
||||
});
|
||||
};
|
||||
|
||||
export const useSyncBulletin = () => {
|
||||
return useMutation({
|
||||
mutationFn: () => sporTotoService.syncFromApi(),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGenerateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: GenerateColumnsDto) => sporTotoService.generateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useEvaluateColumns = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: EvaluateColumnsDto) => sporTotoService.evaluateColumns(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGeneratePrediction = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: GenerateSporTotoPredictionDto) =>
|
||||
sporTotoService.generatePrediction(dto),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import { apiRequest } from "@/lib/api/api-service";
|
||||
import { ApiResponse } from "@/types/api-response";
|
||||
|
||||
/**
|
||||
* Users Service
|
||||
* Backend: /api/users/*
|
||||
*/
|
||||
|
||||
export interface UpdateProfileDto {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordDto {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
export interface UserResponseDto {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
role: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
const getMe = () => {
|
||||
return apiRequest<ApiResponse<UserResponseDto>>({
|
||||
url: "/users/me",
|
||||
client: "core",
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
const updateMe = (dto: UpdateProfileDto) => {
|
||||
return apiRequest<ApiResponse<UserResponseDto>>({
|
||||
url: "/users/me",
|
||||
client: "core",
|
||||
method: "put",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
const changePassword = (dto: ChangePasswordDto) => {
|
||||
return apiRequest<ApiResponse<null>>({
|
||||
url: "/users/me/password",
|
||||
client: "core",
|
||||
method: "patch",
|
||||
data: dto,
|
||||
});
|
||||
};
|
||||
|
||||
export const usersService = {
|
||||
getMe,
|
||||
updateMe,
|
||||
changePassword,
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { usersService } from "./service";
|
||||
import type { UpdateProfileDto, ChangePasswordDto } from "./service";
|
||||
|
||||
export const UsersQueryKeys = {
|
||||
all: ["users"] as const,
|
||||
me: () => [...UsersQueryKeys.all, "me"] as const,
|
||||
};
|
||||
|
||||
export const useUpdateProfile = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: UpdateProfileDto) => usersService.updateMe(dto),
|
||||
});
|
||||
};
|
||||
|
||||
export const useChangePassword = () => {
|
||||
return useMutation({
|
||||
mutationFn: (dto: ChangePasswordDto) => usersService.changePassword(dto),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
interface Pricing {
|
||||
inputCost: number;
|
||||
outputCost: number;
|
||||
}
|
||||
|
||||
export async function getPricing(): Promise<Pricing> {
|
||||
return {
|
||||
inputCost: 0.3,
|
||||
outputCost: 2.5,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { create } from "zustand";
|
||||
import type { CouponStrategy } from "@/lib/api/predictions/types";
|
||||
import type { CouponItemDto } from "@/lib/api/coupons/types";
|
||||
|
||||
interface CouponState {
|
||||
strategy: CouponStrategy;
|
||||
items: CouponItemDto[];
|
||||
isBuilding: boolean;
|
||||
}
|
||||
|
||||
interface CouponActions {
|
||||
setStrategy: (strategy: CouponStrategy) => void;
|
||||
addItem: (item: CouponItemDto) => void;
|
||||
removeItem: (matchId: string) => void;
|
||||
clearCoupon: () => void;
|
||||
setIsBuilding: (isBuilding: boolean) => void;
|
||||
}
|
||||
|
||||
type CouponStore = CouponState & CouponActions;
|
||||
|
||||
const initialState: CouponState = {
|
||||
strategy: "BALANCED",
|
||||
items: [],
|
||||
isBuilding: false,
|
||||
};
|
||||
|
||||
export const useCouponStore = create<CouponStore>()((set) => ({
|
||||
...initialState,
|
||||
|
||||
setStrategy: (strategy) => set({ strategy }),
|
||||
|
||||
addItem: (item) =>
|
||||
set((state) => {
|
||||
// Prevent duplicate match entries
|
||||
const exists = state.items.some((i) => i.matchId === item.matchId);
|
||||
if (exists) return state;
|
||||
return { items: [...state.items, item] };
|
||||
}),
|
||||
|
||||
removeItem: (matchId) =>
|
||||
set((state) => ({
|
||||
items: state.items.filter((i) => i.matchId !== matchId),
|
||||
})),
|
||||
|
||||
clearCoupon: () => set(initialState),
|
||||
|
||||
setIsBuilding: (isBuilding) => set({ isBuilding }),
|
||||
}));
|
||||
@@ -0,0 +1,43 @@
|
||||
import { create } from "zustand";
|
||||
import type { SportType } from "@/lib/api/matches/types";
|
||||
|
||||
interface MatchState {
|
||||
selectedMatchIds: Set<string>;
|
||||
sport: SportType;
|
||||
leagueFilter: string | null;
|
||||
}
|
||||
|
||||
interface MatchActions {
|
||||
toggleMatch: (matchId: string) => void;
|
||||
clearSelection: () => void;
|
||||
setSport: (sport: SportType) => void;
|
||||
setLeague: (leagueId: string | null) => void;
|
||||
isSelected: (matchId: string) => boolean;
|
||||
}
|
||||
|
||||
type MatchStore = MatchState & MatchActions;
|
||||
|
||||
export const useMatchStore = create<MatchStore>()((set, get) => ({
|
||||
selectedMatchIds: new Set<string>(),
|
||||
sport: "football",
|
||||
leagueFilter: null,
|
||||
|
||||
toggleMatch: (matchId) =>
|
||||
set((state) => {
|
||||
const next = new Set(state.selectedMatchIds);
|
||||
if (next.has(matchId)) {
|
||||
next.delete(matchId);
|
||||
} else {
|
||||
next.add(matchId);
|
||||
}
|
||||
return { selectedMatchIds: next };
|
||||
}),
|
||||
|
||||
clearSelection: () => set({ selectedMatchIds: new Set<string>() }),
|
||||
|
||||
setSport: (sport) => set({ sport, leagueFilter: null }),
|
||||
|
||||
setLeague: (leagueId) => set({ leagueFilter: leagueId }),
|
||||
|
||||
isSelected: (matchId) => get().selectedMatchIds.has(matchId),
|
||||
}));
|
||||
@@ -0,0 +1,240 @@
|
||||
import { getPricing } from "@/lib/services/pricing-service";
|
||||
import { GoogleGenAI, Modality } from "@google/genai";
|
||||
|
||||
/**
|
||||
* Merkezi yapay zeka istemci örneği
|
||||
* Tüm yapay zeka istekleri bu örneği kullanmalıdır
|
||||
*/
|
||||
export const ai = new GoogleGenAI({
|
||||
vertexai: true,
|
||||
|
||||
apiKey: process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
|
||||
// Kalite artırıcı anahtar kelimeler
|
||||
const QUALITY_BOOSTERS = [
|
||||
"highly detailed",
|
||||
"8k resolution",
|
||||
"professional photography",
|
||||
"studio lighting",
|
||||
"sharp focus",
|
||||
"cinematic composition",
|
||||
"vibrant colors",
|
||||
"masterpiece",
|
||||
"masterpiece",
|
||||
];
|
||||
|
||||
/**
|
||||
* Yapay Zeka Model yapılandırmaları
|
||||
*/
|
||||
export const AI_MODELS = {
|
||||
FLASH_LITE: "gemini-2.5-flash-lite",
|
||||
FLASH: "gemini-2.5-flash",
|
||||
FLASH_IMAGE: "gemini-2.5-flash-image",
|
||||
} as const;
|
||||
|
||||
// ——————————————————————————————————————
|
||||
// Type definitions for Gemini API responses
|
||||
// ——————————————————————————————————————
|
||||
|
||||
/** Token usage metadata from Gemini API */
|
||||
interface UsageMetadata {
|
||||
promptTokenCount?: number;
|
||||
candidatesTokenCount?: number;
|
||||
totalTokenCount?: number;
|
||||
}
|
||||
|
||||
/** Gemini content part (text or inline data) */
|
||||
interface ContentPart {
|
||||
text?: string;
|
||||
inlineData?: { mimeType: string; data: string };
|
||||
}
|
||||
|
||||
/** Generation config passed to Gemini */
|
||||
interface GenerationConfig {
|
||||
responseModalities?: Modality[];
|
||||
temperature?: number;
|
||||
maxOutputTokens?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Prompt content — can be a string, structured content array, or object */
|
||||
type PromptContent =
|
||||
| string
|
||||
| Array<{ role: string; parts: ContentPart[] }>
|
||||
| Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Yapay zeka ile içerik oluştur
|
||||
* Tüm yapay zeka içerik oluşturma işlemleri için merkezi fonksiyon
|
||||
*
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param prompt - Gönderilecek istem veya içerikler
|
||||
* @param config - İsteğe bağlı yapılandırma
|
||||
* @returns Yapay zeka yanıtı
|
||||
*/
|
||||
export async function generateAIContent(
|
||||
model: string,
|
||||
prompt: PromptContent,
|
||||
config?: GenerationConfig
|
||||
) {
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: prompt,
|
||||
...(config && { config }),
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("AI generation error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yapay zeka ile metin içeriği oluştur
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param prompt - Metin istemi
|
||||
* @returns Oluşturulan metin
|
||||
*/
|
||||
export async function generateText(
|
||||
model: string,
|
||||
prompt: string
|
||||
): Promise<{ text: string; usage?: UsageMetadata }> {
|
||||
const response = await generateAIContent(model, prompt);
|
||||
return {
|
||||
text: (response.text || "").trim(),
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Görüntü girdisi ile içerik oluştur
|
||||
* @param model - Kullanılacak yapay zeka modeli
|
||||
* @param imageBase64 - Base64 kodlanmış görüntü
|
||||
* @param textPrompt - Metin istemi
|
||||
* @returns Yapay zeka yanıtı
|
||||
*/
|
||||
export async function generateWithImage(
|
||||
model: string,
|
||||
imageBase64: string,
|
||||
textPrompt: string
|
||||
) {
|
||||
const response = await generateAIContent(model, [
|
||||
{
|
||||
role: "user",
|
||||
parts: [
|
||||
{ inlineData: { mimeType: "image/jpeg", data: imageBase64 } },
|
||||
{ text: textPrompt },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
text: (response.text || "").trim(),
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* İstemden görüntü oluştur
|
||||
* @param prompt - Görüntü oluşturma için metin istemi
|
||||
* @param imageBase64 - Düzenleme için isteğe bağlı base64 görüntü
|
||||
* @param suggestions - Görsel iyileştirme önerileri (Nano Banana için)
|
||||
* @param model - Kullanılacak model (varsayılan: FLASH_IMAGE)
|
||||
* @param aspectRatio - Hedef en boy oranı (örn: "16:9", "1:1")
|
||||
* @returns Oluşturulan görüntü URL'si (base64)
|
||||
*/
|
||||
export async function generateImage(
|
||||
prompt: string,
|
||||
imageBase64?: string,
|
||||
suggestions?: string[],
|
||||
model: string = AI_MODELS.FLASH_IMAGE,
|
||||
aspectRatio?: string
|
||||
): Promise<{ imageUrl: string | null; usage?: UsageMetadata }> {
|
||||
let parts: Array<
|
||||
{ text: string } | { inlineData: { mimeType: string; data: string } }
|
||||
> = [];
|
||||
|
||||
// En boy oranı talimatı oluştur
|
||||
const ratioInstruction = aspectRatio
|
||||
? `\n\nTarget Aspect Ratio: ${aspectRatio}\nEnsure the image strictly follows the ${aspectRatio} aspect ratio format.`
|
||||
: "";
|
||||
|
||||
if (imageBase64) {
|
||||
// Görüntü düzenleme modu
|
||||
const cleanBase64 = imageBase64.replace(
|
||||
/^data:image\/(png|jpeg|webp|jpg);base64,/,
|
||||
""
|
||||
);
|
||||
|
||||
// Eğer Nano Banana modeli ve öneriler varsa, prompt'u zenginleştir
|
||||
let finalPrompt = prompt;
|
||||
if (model === "nano-banana") {
|
||||
let improvementInstructions = "";
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
improvementInstructions = `\n\nApply these specific improvements based on platform analysis:\n${suggestions.map((s) => `- ${s}`).join("\n")}`;
|
||||
}
|
||||
|
||||
finalPrompt = `${prompt}${improvementInstructions}\n\nStyle & Quality Instructions:\nRender in ${QUALITY_BOOSTERS.join(", ")}.\n\nCRITICAL OBJECTIVE: The result MUST achieve a perfect 10/10 score.\n- Clarity: 10/10 (Ultra-sharp, no blur)\n- Professionalism: 10/10 (High-end commercial look)\n- Engagement: 10/10 (Eye-catching contrast and lighting)\n- Platform Fit: 10/10 (Perfect aspect ratio and framing)\nEnsure the image is visually stunning and flawless.`;
|
||||
}
|
||||
|
||||
parts.push({ inlineData: { mimeType: "image/png", data: cleanBase64 } });
|
||||
parts.push({ text: finalPrompt + ratioInstruction });
|
||||
} else {
|
||||
parts.push({ text: prompt + ratioInstruction });
|
||||
}
|
||||
|
||||
console.log("[AI-Helper] generateImage calling Gemini model", {
|
||||
model: AI_MODELS.FLASH_IMAGE,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await ai.models.generateContent({
|
||||
model,
|
||||
contents: [{ role: "user", parts }],
|
||||
config: {
|
||||
responseModalities: [Modality.IMAGE],
|
||||
},
|
||||
});
|
||||
|
||||
const candidate = response.candidates?.[0];
|
||||
const imagePart = candidate?.content?.parts?.find(
|
||||
(p) => p.inlineData?.mimeType?.startsWith("image/")
|
||||
);
|
||||
|
||||
if (imagePart?.inlineData?.data) {
|
||||
return {
|
||||
imageUrl: `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`,
|
||||
usage: response.usageMetadata as UsageMetadata | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn("No image part found in response");
|
||||
return { imageUrl: null, usage: response.usageMetadata as UsageMetadata | undefined };
|
||||
} catch (error) {
|
||||
console.error("generateImage error:", error);
|
||||
return { imageUrl: null };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token kullanımına dayalı Yapay Zeka Maliyetini hesapla
|
||||
* Giriş Maliyeti = (Giriş Tokenları / 1.000.000) * Giriş Fiyatı
|
||||
* Çıkış Maliyeti = (Çıkış Tokenları / 1.000.000) * Çıkış Fiyatı
|
||||
* @param usage - Yapay zeka yanıtından kullanım meta verileri
|
||||
* @returns Para birimi cinsinden toplam maliyet
|
||||
*/
|
||||
export async function calculateAICost(usage: UsageMetadata | null | undefined): Promise<number> {
|
||||
if (!usage) return 0;
|
||||
|
||||
const pricing = await getPricing();
|
||||
const inputTokens = usage.promptTokenCount || 0;
|
||||
const outputTokens = usage.candidatesTokenCount || 0;
|
||||
|
||||
const inputCost = (inputTokens / 1_000_000) * pricing.inputCost;
|
||||
const outputCost = (outputTokens / 1_000_000) * pricing.outputCost;
|
||||
|
||||
return inputCost + outputCost;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Language mapping utility for AI prompts
|
||||
* Maps locale codes to full language names for AI instructions
|
||||
*/
|
||||
|
||||
export const SUPPORTED_LOCALES = {
|
||||
tr: 'Turkish',
|
||||
en: 'English',
|
||||
de: 'German',
|
||||
ar: 'Arabic',
|
||||
zh: 'Chinese',
|
||||
es: 'Spanish',
|
||||
fr: 'French',
|
||||
it: 'Italian',
|
||||
ja: 'Japanese',
|
||||
ko: 'Korean',
|
||||
pt: 'Portuguese',
|
||||
ru: 'Russian',
|
||||
} as const;
|
||||
|
||||
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES;
|
||||
|
||||
/**
|
||||
* Get the full language name for AI prompts
|
||||
* @param locale - Locale code (e.g., 'tr', 'en')
|
||||
* @returns Full language name (e.g., 'Turkish', 'English')
|
||||
*/
|
||||
export function getLanguageName(locale?: string): string {
|
||||
if (!locale) return SUPPORTED_LOCALES.tr; // Default to Turkish
|
||||
|
||||
const normalizedLocale = locale.toLowerCase() as SupportedLocale;
|
||||
return SUPPORTED_LOCALES[normalizedLocale] || SUPPORTED_LOCALES.en; // Fallback to English
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language instruction for AI prompts
|
||||
* @param locale - Locale code
|
||||
* @returns Formatted instruction for AI (e.g., "Write in Turkish")
|
||||
*/
|
||||
export function getLanguageInstruction(locale?: string): string {
|
||||
const language = getLanguageName(locale);
|
||||
return `Write in ${language}.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a locale is supported
|
||||
* @param locale - Locale code to check
|
||||
* @returns True if supported, false otherwise
|
||||
*/
|
||||
export function isSupportedLocale(locale: string): locale is SupportedLocale {
|
||||
return locale.toLowerCase() in SUPPORTED_LOCALES;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Gemini için Fotoğrafçılık Taslağı (JSON Blueprint)
|
||||
* Bu yapı modeli profesyonel bir fotoğrafçı gibi düşünmeye zorlar.
|
||||
* Client-side güvenlidir.
|
||||
*/
|
||||
export const buildPhotorealisticPrompt = (corePrompt: string): string => {
|
||||
const blueprint = {
|
||||
subject_context: corePrompt,
|
||||
style: {
|
||||
direction: 'Professional Lifestyle Photography / Authentic Brand Content',
|
||||
aesthetic: 'Modern, bright, engaging, premium but approachable',
|
||||
lighting: 'Natural ambient daylight mixed with soft studio fill, warm tones',
|
||||
},
|
||||
camera_settings: {
|
||||
sensor: 'Full-frame Digital Sensor (Sony Alpha / Canon R5)',
|
||||
lens: '35mm or 50mm Prime (Standard Social Media View)',
|
||||
depth_of_field: 'Moderate depth of field (sharp subject, slightly softened background)',
|
||||
shutter: '1/125s natural motion freeze',
|
||||
},
|
||||
composition_rules: {
|
||||
framing: 'Rule of thirds, center-weighted for social media engagement',
|
||||
angle: 'Eye-level or 45-degree isometric (depending on subject)',
|
||||
aspect_ratio_fit: 'Optimized for Instagram/LinkedIn',
|
||||
},
|
||||
quality_assurance: {
|
||||
clarity: 'Perfect Focus',
|
||||
noise_level: 'Minimal natural grain allowed for authenticity',
|
||||
texture_detail: 'High fidelity materials',
|
||||
render_engine: 'Photorealistic Photography Style (Not CGI looking)',
|
||||
},
|
||||
prohibited_elements: ['text watermarks', 'blurry', 'distorted', 'ugly', 'low resolution'],
|
||||
};
|
||||
|
||||
return JSON.stringify(blueprint, null, 2);
|
||||
};
|
||||
Reference in New Issue
Block a user