gg
Deploy Iddaai Frontend / build-and-deploy (push) Failing after 34s

This commit is contained in:
2026-05-10 22:59:27 +03:00
parent 6dadc5f613
commit 5c8619b282
161 changed files with 6708 additions and 3435 deletions
+9
View File
@@ -61,6 +61,14 @@ const resetAllUsageLimits = () => {
});
};
const resetUserUsageLimits = (userId: string) => {
return apiRequest<ApiResponse<null>>({
url: `/admin/usage-limits/reset/${userId}`,
client: "admin",
method: "post",
});
};
// Users
const getAllUsers = (params?: AdminPaginationParams) => {
return apiRequest<ApiResponse<PaginatedData<AdminUserDto>>>({
@@ -119,6 +127,7 @@ export const adminService = {
updateSetting,
getAllUsageLimits,
resetAllUsageLimits,
resetUserUsageLimits,
getAllUsers,
getUserById,
deleteUser,
+2 -1
View File
@@ -49,6 +49,7 @@ export interface AdminUserDto {
role?: string;
isActive: boolean;
subscription?: string;
subscriptionStatus?: string;
createdAt: string;
[key: string]: unknown;
}
@@ -58,7 +59,7 @@ export interface UpdateUserRoleDto {
}
export interface UpdateUserSubscriptionDto {
subscription: string;
plan: string;
}
// ========================
+12
View File
@@ -66,6 +66,18 @@ export const useResetAllUsageLimits = () => {
});
};
export const useResetUserUsageLimits = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) => adminService.resetUserUsageLimits(userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.usageLimits() });
queryClient.invalidateQueries({ queryKey: AdminQueryKeys.users() });
},
});
};
// Users
export const useAdminUsers = (
params?: AdminPaginationParams,
+5 -1
View File
@@ -1,6 +1,10 @@
import { apiRequest } from "@/lib/api/api-service";
import { ApiResponse } from "@/types/api-response";
import type { AnalyzeMatchesDto, AnalysisResultDto, AnalysisHistoryDto } from "./types";
import type {
AnalyzeMatchesDto,
AnalysisResultDto,
AnalysisHistoryDto,
} from "./types";
/**
* Analysis Service
+12 -5
View File
@@ -1,5 +1,5 @@
import { clientMap } from '@/lib/api/client-map';
import { Method } from 'axios';
import { clientMap } from "@/lib/api/client-map";
import { Method } from "axios";
interface ApiRequestOptions {
url: string;
@@ -9,14 +9,21 @@ interface ApiRequestOptions {
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;
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 });
const response = await clientInstance.request<T>({
method,
url,
data,
params,
});
return response.data;
}
+1 -6
View File
@@ -1,11 +1,6 @@
import { apiRequest } from "@/lib/api/api-service";
import { ApiResponse } from "@/types/api-response";
import {
LoginDto,
AuthResponse,
RegisterDto,
RefreshTokenDto,
} from "./types";
import { LoginDto, AuthResponse, RegisterDto, RefreshTokenDto } from "./types";
const login = (data: LoginDto) => {
return apiRequest<ApiResponse<AuthResponse>>({
+3 -3
View File
@@ -1,6 +1,6 @@
import baseUrl from '@/config/base-url';
import { createApiClient } from '@/lib/api/create-api-client';
import { AxiosInstance } from 'axios';
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!),
-1
View File
@@ -90,4 +90,3 @@ export const couponsService = {
suggestCoupon,
generateFrequencyCoupon,
};
-1
View File
@@ -63,4 +63,3 @@ export const useGenerateFrequencyCoupon = () => {
couponsService.generateFrequencyCoupon(dto),
});
};
+11 -5
View File
@@ -171,10 +171,10 @@ export interface OddsBandEntryDto {
odd_rate?: number;
even_rate?: number;
"1x_rate"?: number;
"x2_rate"?: number;
x2_rate?: number;
"12_rate"?: number;
"1x_sample"?: number;
"x2_sample"?: number;
x2_sample?: number;
"12_sample"?: number;
sample: number;
[key: string]: unknown;
@@ -208,9 +208,15 @@ export interface TripleValueEntryDto {
}
export type HtftComboKey =
| "11" | "1x" | "12"
| "x1" | "xx" | "x2"
| "21" | "2x" | "22";
| "11"
| "1x"
| "12"
| "x1"
| "xx"
| "x2"
| "21"
| "2x"
| "22";
export interface V27EngineDto {
version: string;
+1 -1
View File
@@ -17,7 +17,7 @@ export const usePrediction = (matchId: string) => {
return useQuery({
queryKey: PredictionsQueryKeys.detail(matchId),
queryFn: () => predictionsService.getPrediction(matchId),
enabled: !!matchId,
enabled: false, // DO NOT auto-fetch since it consumes limits
});
};
+4 -2
View File
@@ -55,13 +55,15 @@ export const useSyncBulletin = () => {
export const useGenerateColumns = () => {
return useMutation({
mutationFn: (dto: GenerateColumnsDto) => sporTotoService.generateColumns(dto),
mutationFn: (dto: GenerateColumnsDto) =>
sporTotoService.generateColumns(dto),
});
};
export const useEvaluateColumns = () => {
return useMutation({
mutationFn: (dto: EvaluateColumnsDto) => sporTotoService.evaluateColumns(dto),
mutationFn: (dto: EvaluateColumnsDto) =>
sporTotoService.evaluateColumns(dto),
});
};
+22
View File
@@ -0,0 +1,22 @@
export { subscriptionService } from "./service";
export {
useSubscriptionPlans,
useMySubscription,
useCheckoutConfig,
useCancelSubscription,
SubscriptionQueryKeys,
} from "./use-hooks";
export type {
PlanType,
BillingInterval,
PlanInfo,
SubscriptionDto,
UsageLimitDto,
CheckoutConfigDto,
CreateCheckoutRequest,
CancelSubscriptionRequest,
} from "./types";
export {
PlanType as PlanTypeEnum,
BillingInterval as BillingIntervalEnum,
} from "./types";
+59
View File
@@ -0,0 +1,59 @@
import { apiRequest } from "@/lib/api/api-service";
import type { ApiResponse } from "@/types/api-response";
import type {
PlanInfo,
SubscriptionDto,
CheckoutConfigDto,
CreateCheckoutRequest,
CancelSubscriptionRequest,
} from "./types";
/**
* Subscriptions Service
* Backend: /api/subscriptions/*
*/
const getPlans = (): Promise<ApiResponse<readonly PlanInfo[]>> => {
return apiRequest<ApiResponse<readonly PlanInfo[]>>({
url: "/subscriptions/plans",
client: "core",
method: "get",
});
};
const getMySubscription = (): Promise<ApiResponse<SubscriptionDto | null>> => {
return apiRequest<ApiResponse<SubscriptionDto | null>>({
url: "/subscriptions/me",
client: "core",
method: "get",
});
};
const getCheckoutConfig = (
dto: CreateCheckoutRequest,
): Promise<ApiResponse<CheckoutConfigDto>> => {
return apiRequest<ApiResponse<CheckoutConfigDto>>({
url: "/subscriptions/checkout",
client: "core",
method: "post",
data: dto,
});
};
const cancelSubscription = (
dto: CancelSubscriptionRequest,
): Promise<ApiResponse<null>> => {
return apiRequest<ApiResponse<null>>({
url: "/subscriptions/cancel",
client: "core",
method: "post",
data: dto,
});
};
export const subscriptionService = {
getPlans,
getMySubscription,
getCheckoutConfig,
cancelSubscription,
};
+79
View File
@@ -0,0 +1,79 @@
// ========================
// Enums — mirrors backend PlanType / BillingIntervalType
// ========================
export enum PlanType {
FREE = "free",
PLUS = "plus",
PREMIUM = "premium",
}
export enum BillingInterval {
MONTHLY = "monthly",
YEARLY = "yearly",
}
// ========================
// Response DTOs
// ========================
export interface UsageLimitDto {
analysisCount: number;
couponCount: number;
maxAnalyses: number;
maxCoupons: number;
}
export interface SubscriptionDto {
id: string;
plan: PlanType;
billingInterval: BillingInterval | null;
currentPeriodStart: string | null;
currentPeriodEnd: string | null;
cancelledAt: string | null;
cancelEffectiveDate: string | null;
paddlePriceId: string | null;
createdAt: string;
updatedAt: string;
}
export interface PlanLimits {
maxAnalyses: number;
maxCoupons: number;
}
export interface PlanInfo {
id: PlanType;
name: string;
description: string;
monthlyPrice: number;
yearlyPrice: number;
currency: string;
features: string[];
limits: PlanLimits;
highlighted: boolean;
}
// ========================
// Request DTOs
// ========================
export interface CreateCheckoutRequest {
plan: PlanType;
billingInterval: BillingInterval;
}
export interface CancelSubscriptionRequest {
reason?: string;
}
// ========================
// Checkout Config Response
// ========================
export interface CheckoutConfigDto {
priceId: string;
clientToken: string;
environment: string;
userId: string;
}
+58
View File
@@ -0,0 +1,58 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { subscriptionService } from "./service";
import type { CreateCheckoutRequest, CancelSubscriptionRequest } from "./types";
export const SubscriptionQueryKeys = {
all: ["subscriptions"] as const,
plans: () => [...SubscriptionQueryKeys.all, "plans"] as const,
me: () => [...SubscriptionQueryKeys.all, "me"] as const,
};
/**
* Fetch all available plans (public, no auth required)
*/
export const useSubscriptionPlans = () => {
return useQuery({
queryKey: SubscriptionQueryKeys.plans(),
queryFn: () => subscriptionService.getPlans(),
staleTime: 1000 * 60 * 30, // Plans rarely change — 30 min cache
});
};
/**
* Fetch current user subscription status
*/
export const useMySubscription = (enabled = true) => {
return useQuery({
queryKey: SubscriptionQueryKeys.me(),
queryFn: () => subscriptionService.getMySubscription(),
enabled,
});
};
/**
* Get Paddle checkout config for a plan
*/
export const useCheckoutConfig = () => {
return useMutation({
mutationFn: (dto: CreateCheckoutRequest) =>
subscriptionService.getCheckoutConfig(dto),
});
};
/**
* Cancel the current subscription
*/
export const useCancelSubscription = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CancelSubscriptionRequest) =>
subscriptionService.cancelSubscription(dto),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: SubscriptionQueryKeys.me(),
});
},
});
};
+9
View File
@@ -16,6 +16,14 @@ export interface ChangePasswordDto {
newPassword: string;
}
export interface UsageLimitDto {
analysisCount: number;
couponCount: number;
maxAnalyses: number;
maxCoupons: number;
lastResetDate: string;
}
export interface UserResponseDto {
id: string;
email: string;
@@ -25,6 +33,7 @@ export interface UserResponseDto {
isActive: boolean;
createdAt: string;
updatedAt: string;
usageLimit?: UsageLimitDto;
}
const getMe = () => {
+9 -1
View File
@@ -1,4 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQuery } from "@tanstack/react-query";
import { usersService } from "./service";
import type { UpdateProfileDto, ChangePasswordDto } from "./service";
@@ -7,6 +7,14 @@ export const UsersQueryKeys = {
me: () => [...UsersQueryKeys.all, "me"] as const,
};
export const useGetMe = (enabled: boolean = true) => {
return useQuery({
queryKey: UsersQueryKeys.me(),
queryFn: () => usersService.getMe(),
enabled,
});
};
export const useUpdateProfile = () => {
return useMutation({
mutationFn: (dto: UpdateProfileDto) => usersService.updateMe(dto),
+11 -17
View File
@@ -9,7 +9,9 @@ function randomToken() {
return Math.random().toString(36).substring(2) + Date.now().toString(36);
}
const isMockMode = process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
const isMockMode =
process.env.NODE_ENV !== "production" &&
process.env.NEXT_PUBLIC_ENABLE_MOCK_MODE === "true";
export const authOptions: NextAuthOptions = {
providers: [
@@ -21,51 +23,42 @@ export const authOptions: NextAuthOptions = {
},
async authorize(credentials) {
try {
console.log("Starting authorization with:", {
email: credentials?.email,
});
if (!credentials?.email || !credentials?.password) {
throw new Error("Email ve şifre gereklidir.");
}
// Eğer mock mod aktifse backend'e gitme
if (isMockMode) {
console.log("Mock mode active, bypassing backend");
return {
id: credentials.email,
name: credentials.email.split("@")[0],
email: credentials.email,
accessToken: randomToken(),
refreshToken: randomToken(),
subscriptionPlan: "free",
};
}
// Normal mod: backend'e istek at
console.log("Sending login request to backend...");
const res = await authService.login({
email: credentials.email,
password: credentials.password,
});
console.log(
"Backend response received:",
JSON.stringify(res, null, 2),
);
const response = res;
// Backend returns ApiResponse<TokenResponseDto>
// Structure: { data: { accessToken, refreshToken, expiresIn, user }, message, statusCode }
if (!res.success || !response?.data?.accessToken) {
console.error("Login failed or no access token in response");
throw new Error(response?.message || "Giriş başarısız");
}
const { accessToken, refreshToken, user } = response.data;
const normalizedRoles = normalizeRoles(user.roles);
console.log("Login successful, creating user session object");
// Extract subscription plan from user data (backend includes subscriptionStatus)
const subscriptionPlan = (user as Record<string, unknown>)
.subscriptionPlan as "free" | "plus" | "premium" | undefined;
return {
id: user.id,
@@ -76,15 +69,14 @@ export const authOptions: NextAuthOptions = {
accessToken,
refreshToken,
roles: normalizedRoles,
subscriptionPlan: subscriptionPlan ?? "free",
};
} catch (error: unknown) {
console.error("Authorize error detailed:", error);
const err = error as Error & {
response?: { data: unknown; status: number };
};
if (err.response) {
console.error("Error response data:", err.response.data);
console.error("Error response status:", err.response.status);
// keep error logging for actual errors but remove verbose debugging
}
throw new Error(
err.message || "An error occurred during authentication",
@@ -100,12 +92,14 @@ export const authOptions: NextAuthOptions = {
token.refreshToken = user.refreshToken;
token.id = user.id;
token.roles = normalizeRoles(user.roles);
token.subscriptionPlan = user.subscriptionPlan ?? "free";
}
return token;
},
async session({ session, token }: { session: Session; token: JWT }) {
session.user.id = token.id;
session.user.roles = normalizeRoles(token.roles);
session.user.subscriptionPlan = token.subscriptionPlan ?? "free";
session.accessToken = token.accessToken;
session.refreshToken = token.refreshToken;
return session;
+4
View File
@@ -0,0 +1,4 @@
export { PaddleProvider, usePaddle, CheckoutEventNames } from "./paddle-provider";
export type { PaddleEventData } from "./paddle-provider";
export { usePaddleCheckout } from "./use-paddle-checkout";
export { paddleConfig } from "./paddle-config";
+19
View File
@@ -0,0 +1,19 @@
/**
* Paddle client-side configuration.
*
* These values are read from NEXT_PUBLIC_* env vars so they are
* available in the browser bundle.
*/
export const paddleConfig = {
/** Paddle client-side token (public — safe for browser) */
clientToken: process.env.NEXT_PUBLIC_PADDLE_CLIENT_TOKEN ?? "",
/** 'sandbox' | 'production' */
environment: (process.env.NEXT_PUBLIC_PADDLE_ENVIRONMENT ?? "sandbox") as
| "sandbox"
| "production",
/** Paddle seller ID — used for Paddle.js initialization */
sellerId: process.env.NEXT_PUBLIC_PADDLE_SELLER_ID ?? "",
} as const;
+108
View File
@@ -0,0 +1,108 @@
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
useRef,
type ReactNode,
} from "react";
import type {
Paddle,
CheckoutOpenOptions,
} from "@paddle/paddle-js";
import { initializePaddle } from "@paddle/paddle-js";
import type { PaddleEventData } from "@paddle/paddle-js/types/checkout/checkout";
import { CheckoutEventNames } from "@paddle/paddle-js/types/checkout/events";
import { paddleConfig } from "./paddle-config";
type CheckoutEventHandler = (event: PaddleEventData) => void;
interface PaddleContextValue {
paddle: Paddle | null;
isReady: boolean;
openCheckout: (options: CheckoutOpenOptions) => void;
onCheckoutEvent: (handler: CheckoutEventHandler) => () => void;
}
const PaddleContext = createContext<PaddleContextValue>({
paddle: null,
isReady: false,
openCheckout: () => {},
onCheckoutEvent: () => () => {},
});
export function PaddleProvider({ children }: { children: ReactNode }) {
const [paddle, setPaddle] = useState<Paddle | null>(null);
const [isReady, setIsReady] = useState(false);
const listenersRef = useRef<Set<CheckoutEventHandler>>(new Set());
useEffect(() => {
if (!paddleConfig.clientToken) {
console.warn("[Paddle] Missing NEXT_PUBLIC_PADDLE_CLIENT_TOKEN");
return;
}
initializePaddle({
token: paddleConfig.clientToken,
environment: paddleConfig.environment,
eventCallback: (event: PaddleEventData) => {
listenersRef.current.forEach((handler) => handler(event));
},
})
.then((paddleInstance) => {
if (paddleInstance) {
setPaddle(paddleInstance);
setIsReady(true);
}
})
.catch((err: unknown) => {
console.error("[Paddle] Initialization failed:", err);
});
}, []);
const openCheckout = useCallback(
(options: CheckoutOpenOptions) => {
if (!paddle) {
console.error("[Paddle] Not initialized — cannot open checkout");
return;
}
paddle.Checkout.open(options);
},
[paddle],
);
/**
* Subscribe to Paddle checkout events.
* Returns an unsubscribe function for cleanup.
*/
const onCheckoutEvent = useCallback(
(handler: CheckoutEventHandler): (() => void) => {
listenersRef.current.add(handler);
return () => {
listenersRef.current.delete(handler);
};
},
[],
);
return (
<PaddleContext.Provider
value={{ paddle, isReady, openCheckout, onCheckoutEvent }}
>
{children}
</PaddleContext.Provider>
);
}
/**
* Hook to access the Paddle instance, checkout helper, and event subscription.
*/
export function usePaddle(): PaddleContextValue {
return useContext(PaddleContext);
}
export { CheckoutEventNames };
export type { PaddleEventData };
+99
View File
@@ -0,0 +1,99 @@
"use client";
import { useCallback, useEffect, useRef } from "react";
import { useSession } from "next-auth/react";
import { usePaddle, CheckoutEventNames } from "./paddle-provider";
import type { PaddleEventData } from "./paddle-provider";
import { useCheckoutConfig } from "@/lib/api/subscriptions/use-hooks";
import type { PlanType, BillingInterval } from "@/lib/api/subscriptions/types";
import { useQueryClient } from "@tanstack/react-query";
import { SubscriptionQueryKeys } from "@/lib/api/subscriptions/use-hooks";
interface UsePaddleCheckoutReturn {
startCheckout: (plan: PlanType, interval: BillingInterval) => Promise<void>;
isLoading: boolean;
}
/**
* Hook that orchestrates the Paddle checkout flow:
* 1. Calls backend to get priceId + clientToken
* 2. Opens Paddle overlay checkout
* 3. Listens for Paddle checkout.completed event to invalidate subscription cache
*/
export function usePaddleCheckout(): UsePaddleCheckoutReturn {
const { openCheckout, onCheckoutEvent } = usePaddle();
const { data: session } = useSession();
const checkoutConfig = useCheckoutConfig();
const queryClient = useQueryClient();
const isSubscribedRef = useRef(false);
// Subscribe to Paddle events once on mount
useEffect(() => {
if (isSubscribedRef.current) return;
isSubscribedRef.current = true;
const unsubscribe = onCheckoutEvent((event: PaddleEventData) => {
if (event.name === CheckoutEventNames.CHECKOUT_COMPLETED) {
// Paddle confirmed the payment — refresh subscription state
queryClient.invalidateQueries({
queryKey: SubscriptionQueryKeys.me(),
});
}
if (event.name === CheckoutEventNames.CHECKOUT_CLOSED) {
// User closed checkout — refresh in case a payment was already processed
queryClient.invalidateQueries({
queryKey: SubscriptionQueryKeys.me(),
});
}
});
return () => {
isSubscribedRef.current = false;
unsubscribe();
};
}, [onCheckoutEvent, queryClient]);
const startCheckout = useCallback(
async (plan: PlanType, interval: BillingInterval) => {
if (!session?.user?.email) {
console.error("[Paddle] User not authenticated");
return;
}
const result = await checkoutConfig.mutateAsync({
plan,
billingInterval: interval,
});
if (!result.success || !result.data) {
console.error("[Paddle] Failed to get checkout config");
return;
}
const { priceId } = result.data;
openCheckout({
items: [{ priceId, quantity: 1 }],
customer: {
email: session.user.email,
},
customData: {
userId: session.user.id,
},
settings: {
displayMode: "overlay",
theme: "dark",
locale: "tr",
successUrl: `${window.location.origin}/profile?checkout=success`,
},
});
},
[openCheckout, session, checkoutConfig],
);
return {
startCheckout,
isLoading: checkoutConfig.isPending,
};
}
+14 -9
View File
@@ -76,7 +76,7 @@ type PromptContent =
export async function generateAIContent(
model: string,
prompt: PromptContent,
config?: GenerationConfig
config?: GenerationConfig,
) {
try {
const response = await ai.models.generateContent({
@@ -100,7 +100,7 @@ export async function generateAIContent(
*/
export async function generateText(
model: string,
prompt: string
prompt: string,
): Promise<{ text: string; usage?: UsageMetadata }> {
const response = await generateAIContent(model, prompt);
return {
@@ -119,7 +119,7 @@ export async function generateText(
export async function generateWithImage(
model: string,
imageBase64: string,
textPrompt: string
textPrompt: string,
) {
const response = await generateAIContent(model, [
{
@@ -151,7 +151,7 @@ export async function generateImage(
imageBase64?: string,
suggestions?: string[],
model: string = AI_MODELS.FLASH_IMAGE,
aspectRatio?: string
aspectRatio?: string,
): Promise<{ imageUrl: string | null; usage?: UsageMetadata }> {
let parts: Array<
{ text: string } | { inlineData: { mimeType: string; data: string } }
@@ -166,7 +166,7 @@ export async function generateImage(
// 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
@@ -200,8 +200,8 @@ export async function generateImage(
});
const candidate = response.candidates?.[0];
const imagePart = candidate?.content?.parts?.find(
(p) => p.inlineData?.mimeType?.startsWith("image/")
const imagePart = candidate?.content?.parts?.find((p) =>
p.inlineData?.mimeType?.startsWith("image/"),
);
if (imagePart?.inlineData?.data) {
@@ -212,7 +212,10 @@ export async function generateImage(
}
console.warn("No image part found in response");
return { imageUrl: null, usage: response.usageMetadata as UsageMetadata | undefined };
return {
imageUrl: null,
usage: response.usageMetadata as UsageMetadata | undefined,
};
} catch (error) {
console.error("generateImage error:", error);
return { imageUrl: null };
@@ -226,7 +229,9 @@ export async function generateImage(
* @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> {
export async function calculateAICost(
usage: UsageMetadata | null | undefined,
): Promise<number> {
if (!usage) return 0;
const pricing = await getPricing();
+12 -12
View File
@@ -4,18 +4,18 @@
*/
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',
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;
+23 -15
View File
@@ -7,28 +7,36 @@ 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',
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',
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',
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)',
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'],
prohibited_elements: [
"text watermarks",
"blurry",
"distorted",
"ugly",
"low resolution",
],
};
return JSON.stringify(blueprint, null, 2);