diff --git a/src/app/dashboard/admin/data/data-client.tsx b/src/app/dashboard/admin/data/data-client.tsx new file mode 100644 index 0000000..4e9c95c --- /dev/null +++ b/src/app/dashboard/admin/data/data-client.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { AdminNav } from "@/components/admin/admin-nav"; +import { ApifyIngestForm } from "@/components/admin/apify-ingest-form"; +import { useLang } from "@/lib/use-lang"; +import { useT } from "@/lib/i18n"; + +export type JobRow = { id: string; source: string; type: string; status: string; recordsImported: number; recordsFailed: number; errorMessage: string | null }; + +export function AdminDataClient({ ads, stores, users, jobs, configured }: { ads: number; stores: number; users: number; jobs: JobRow[]; configured: boolean }) { + const [lang] = useLang(); + const t = useT(); + void lang; + return

{t("adminData.title")}

{t("adminData.subtitle")}

{ads}
{t("adminData.ads")}
{stores}
{t("adminData.stores")}
{users}
{t("adminData.users")}

{t("adminData.metaAdsTitle")}

{t("adminData.metaAdsDesc")}

{t("adminData.ingestJobs")}

{jobs.map((j) =>
{j.source} · {j.type}
{j.status} · {j.recordsImported} {t("adminData.ok")} · {j.recordsFailed} {t("adminData.failed")}
{j.errorMessage &&
{j.errorMessage}
}
)}{!jobs.length &&

{t("adminData.noJobs")}

}
; +} diff --git a/src/app/dashboard/admin/data/page.tsx b/src/app/dashboard/admin/data/page.tsx index 74736f1..a6d96b5 100644 --- a/src/app/dashboard/admin/data/page.tsx +++ b/src/app/dashboard/admin/data/page.tsx @@ -1,11 +1,9 @@ +import { AdminDataClient } from "./data-client"; import { requireAdmin } from "@/lib/auth/current-user"; import { prisma } from "@/lib/db"; -import { Card } from "@/components/ui/card"; -import { AdminNav } from "@/components/admin/admin-nav"; -import { ApifyIngestForm } from "@/components/admin/apify-ingest-form"; export default async function AdminDataPage() { await requireAdmin(); const [ads, stores, users, jobs] = await Promise.all([prisma.ad.count(), prisma.store.count(), prisma.user.count(), prisma.ingestJob.findMany({ orderBy: { createdAt: "desc" }, take: 10 })]); - return

Veri İşleri

Reklam, mağaza ve ingest operasyonları.

{ads}
Ads
{stores}
Stores
{users}
Users

Meta Ads Library

Anahtar kelimeyle gerçek Meta reklamlarını çekin. Her çalışmada maliyet üst sınırı uygulanır ve tekrar eden reklamlar güncellenir.

Ingest Jobs

{jobs.map((j) =>
{j.source === "apify" ? "Meta veri kaynağı" : j.source} · {j.type}
{j.status} · {j.recordsImported} başarılı · {j.recordsFailed} hatalı
{j.errorMessage &&
{j.errorMessage.replace(/APIFY/g, "VERI_KAYNAGI")}
}
)}{!jobs.length &&

Henüz ingest işi yok.

}
; + return ({ id: j.id, source: j.source === "apify" ? "Meta veri kaynağı" : j.source, type: j.type, status: j.status, recordsImported: j.recordsImported, recordsFailed: j.recordsFailed, errorMessage: j.errorMessage ? j.errorMessage.replace(/APIFY/g, "VERI_KAYNAGI") : null }))} />; } diff --git a/src/app/dashboard/admin/logs/logs-client.tsx b/src/app/dashboard/admin/logs/logs-client.tsx new file mode 100644 index 0000000..2289aa2 --- /dev/null +++ b/src/app/dashboard/admin/logs/logs-client.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { AdminNav } from "@/components/admin/admin-nav"; +import { Card } from "@/components/ui/card"; +import { useLang } from "@/lib/use-lang"; +import { useT } from "@/lib/i18n"; + +export type AuditRow = { id: string; action: string; summary: string; actorEmail: string; ipLabel: string; createdLabel: string }; +export type CreditRow = { id: string; email: string; type: string; amount: number; balanceAfter: number; reason: string; createdLabel: string }; +export type IngestRow = { id: string; source: string; type: string; status: string; recordsImported: number; recordsFailed: number; errorMessage: string | null }; +export type ExportRow = { id: string; email: string; module: string; status: string; createdLabel: string; errorMessage: string | null }; +export type SessionRow = { id: string; email: string; createdLabel: string; expiresLabel: string }; + +export function AdminLogsClient({ audits, credits, ingestJobs, exportJobs, sessions }: { audits: AuditRow[]; credits: CreditRow[]; ingestJobs: IngestRow[]; exportJobs: ExportRow[]; sessions: SessionRow[] }) { + const [lang] = useLang(); + const t = useT(); + void lang; + return ( +
+

{t("adminLogs.title")}

{t("adminLogs.subtitle")}

+ +
+

{t("adminLogs.auditTitle")}

{audits.map((log) =>
{log.action}{log.createdLabel}

{log.summary}

{log.actorEmail} · {log.ipLabel}
)}{!audits.length &&

{t("adminLogs.noAudit")}

}
+

{t("adminLogs.creditsTitle")}

{credits.map((tx) => )}
{t("adminLogs.user")}{t("adminLogs.type")}{t("adminLogs.amount")}{t("adminLogs.balance")}{t("adminLogs.reason")}{t("adminLogs.date")}
{tx.email}{tx.type}= 0 ? "font-bold text-emerald-600" : "font-bold text-rose-600"}>{tx.amount > 0 ? "+" : ""}{tx.amount}{tx.balanceAfter}{tx.reason}{tx.createdLabel}
+

{t("adminLogs.ingestTitle")}

{ingestJobs.map((job) =>
{job.source} · {job.type}
{job.status} · {job.recordsImported} {t("adminLogs.ok")} · {job.recordsFailed} {t("adminLogs.failed")}
{job.errorMessage &&
{job.errorMessage}
}
)}

{t("adminLogs.exportTitle")}

{exportJobs.map((job) =>
{job.email} · {job.module}
{job.status} · {job.createdLabel}
{job.errorMessage &&
{job.errorMessage}
}
)}
+

{t("adminLogs.sessionsTitle")}

{sessions.map((session) =>
{session.email}
{t("adminLogs.opened")}: {session.createdLabel} · {t("adminLogs.expires")}: {session.expiresLabel}
)}
+
+
+ ); +} diff --git a/src/app/dashboard/admin/logs/page.tsx b/src/app/dashboard/admin/logs/page.tsx index 75e19b5..4f5c85d 100644 --- a/src/app/dashboard/admin/logs/page.tsx +++ b/src/app/dashboard/admin/logs/page.tsx @@ -1,5 +1,4 @@ -import { AdminNav } from "@/components/admin/admin-nav"; -import { Card } from "@/components/ui/card"; +import { AdminLogsClient } from "./logs-client"; import { requireAdmin } from "@/lib/auth/current-user"; import { prisma } from "@/lib/db"; @@ -13,15 +12,12 @@ export default async function AdminLogsPage() { prisma.authSession.findMany({ include: { user: { select: { email: true } } }, orderBy: { createdAt: "desc" }, take: 50 }) ]); return ( -
-

Log Merkezi

Admin audit, kredi hareketleri, veri işleri ve oturum kayıtları.

- -
-

Admin audit logları

{audits.map((log) =>
{log.action.replace(/APIFY/g, "META_DATA")}{log.createdAt.toLocaleString("tr-TR")}

{log.summary.replace(/Apify/gi, "veri kaynağı")}

{log.actorEmail} · {log.ipAddress || "IP yok"}
)}{!audits.length &&

Audit kaydı yok.

}
-

Kredi hareketleri

{credits.map((tx) => )}
KullanıcıTürMiktarBakiyeNedenTarih
{tx.user.email}{tx.type}= 0 ? "font-bold text-emerald-600" : "font-bold text-rose-600"}>{tx.amount > 0 ? "+" : ""}{tx.amount}{tx.balanceAfter}{tx.reason}{tx.createdAt.toLocaleString("tr-TR")}
-

Ingest işleri

{ingestJobs.map((job) =>
{job.source === "apify" ? "Meta veri kaynağı" : job.source} · {job.type}
{job.status} · {job.recordsImported} başarılı · {job.recordsFailed} hatalı
{job.errorMessage &&
{job.errorMessage.replace(/APIFY/g, "VERI_KAYNAGI")}
}
)}

Export işleri

{exportJobs.map((job) =>
{job.user.email} · {job.module}
{job.status} · {job.createdAt.toLocaleString("tr-TR")}
{job.errorMessage &&
{job.errorMessage}
}
)}
-

Son oturumlar

{sessions.map((session) =>
{session.user.email}
Açılış: {session.createdAt.toLocaleString("tr-TR")} · Bitiş: {session.expiresAt.toLocaleString("tr-TR")}
)}
-
-
+ ({ id: log.id, action: log.action.replace(/APIFY/g, "META_DATA"), summary: log.summary.replace(/Apify/gi, "veri kaynağı"), actorEmail: log.actorEmail, ipLabel: log.ipAddress || "IP yok", createdLabel: log.createdAt.toLocaleString("tr-TR") }))} + credits={credits.map((tx) => ({ id: tx.id, email: tx.user.email, type: tx.type, amount: tx.amount, balanceAfter: tx.balanceAfter, reason: tx.reason, createdLabel: tx.createdAt.toLocaleString("tr-TR") }))} + ingestJobs={ingestJobs.map((job) => ({ id: job.id, source: job.source === "apify" ? "Meta veri kaynağı" : job.source, type: job.type, status: job.status, recordsImported: job.recordsImported, recordsFailed: job.recordsFailed, errorMessage: job.errorMessage ? job.errorMessage.replace(/APIFY/g, "VERI_KAYNAGI") : null }))} + exportJobs={exportJobs.map((job) => ({ id: job.id, email: job.user.email, module: job.module, status: job.status, createdLabel: job.createdAt.toLocaleString("tr-TR"), errorMessage: job.errorMessage }))} + sessions={sessions.map((session) => ({ id: session.id, email: session.user.email, createdLabel: session.createdAt.toLocaleString("tr-TR"), expiresLabel: session.expiresAt.toLocaleString("tr-TR") }))} + /> ); } diff --git a/src/app/dashboard/admin/payments/page.tsx b/src/app/dashboard/admin/payments/page.tsx index dd03b64..db9f000 100644 --- a/src/app/dashboard/admin/payments/page.tsx +++ b/src/app/dashboard/admin/payments/page.tsx @@ -1,6 +1,4 @@ -import { AdminNav } from "@/components/admin/admin-nav"; -import { ManualPaymentForm, PaymentStatusForm } from "@/components/admin/admin-actions"; -import { Card } from "@/components/ui/card"; +import { AdminPaymentsClient } from "./payments-client"; import { requireAdmin } from "@/lib/auth/current-user"; import { prisma } from "@/lib/db"; @@ -13,14 +11,19 @@ export default async function AdminPaymentsPage() { prisma.payment.findMany({ include: { user: { select: { email: true } }, actor: { select: { email: true } } }, orderBy: { createdAt: "desc" }, take: 100 }) ]); return ( -
-

Ödemeler

Manuel tahsilat, referans ve ödeme durumlarını yönetin.

- -

Manuel ödeme ekle

Ödeme ile verilen kredi atomik olarak bakiyeye eklenir. İade durumunda kredi otomatik düşmez; kullanıcı ekranından gerekçeli işlem yapın.

- -
{payments.map((payment) => )}
KullanıcıTutarSağlayıcıKrediReferansTarihDurum
{payment.user.email}
Admin: {payment.actor?.email || "Sistem"}
{money(payment.amountCents, payment.currency)}{payment.provider}{payment.creditGranted}{payment.externalId || "—"}{payment.createdAt.toLocaleString("tr-TR")}
- {!payments.length &&
Henüz ödeme kaydı yok.
} -
-
+ ({ + id: p.id, + userEmail: p.user.email, + actorEmail: p.actor?.email || null, + amountLabel: money(p.amountCents, p.currency), + provider: p.provider, + creditGranted: p.creditGranted, + externalId: p.externalId || null, + createdLabel: p.createdAt.toLocaleString("tr-TR"), + status: p.status, + }))} + /> ); } diff --git a/src/app/dashboard/admin/payments/payments-client.tsx b/src/app/dashboard/admin/payments/payments-client.tsx new file mode 100644 index 0000000..3315b5d --- /dev/null +++ b/src/app/dashboard/admin/payments/payments-client.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { AdminNav } from "@/components/admin/admin-nav"; +import { ManualPaymentForm, PaymentStatusForm } from "@/components/admin/admin-actions"; +import { Card } from "@/components/ui/card"; +import { useLang } from "@/lib/use-lang"; +import { useT } from "@/lib/i18n"; + +type UserOption = { id: string; email: string }; +type PaymentRow = { + id: string; + userEmail: string; + actorEmail: string | null; + amountLabel: string; + provider: string; + creditGranted: number; + externalId: string | null; + createdLabel: string; + status: string; +}; + +export function AdminPaymentsClient({ users, payments }: { users: UserOption[]; payments: PaymentRow[] }) { + const [lang] = useLang(); + const t = useT(); + void lang; + return ( +
+

{t("adminPayments.title")}

{t("adminPayments.subtitle")}

+ +

{t("adminPayments.addManual")}

{t("adminPayments.atomicNote")}

+ +
{payments.map((payment) => )}
{t("adminPayments.user")}{t("adminPayments.amount")}{t("adminPayments.provider")}{t("adminPayments.credit")}{t("adminPayments.reference")}{t("adminPayments.date")}{t("adminPayments.status")}
{payment.userEmail}
{t("adminPayments.adminLabel")}: {payment.actorEmail || t("adminPayments.system")}
{payment.amountLabel}{payment.provider}{payment.creditGranted}{payment.externalId || "—"}{payment.createdLabel}
+ {!payments.length &&
{t("adminPayments.noRecords")}
} +
+
+ ); +} diff --git a/src/app/dashboard/admin/users/page.tsx b/src/app/dashboard/admin/users/page.tsx index 14c5890..41961c6 100644 --- a/src/app/dashboard/admin/users/page.tsx +++ b/src/app/dashboard/admin/users/page.tsx @@ -1,7 +1,5 @@ import { Prisma, UserRole } from "@prisma/client"; -import { AdminNav } from "@/components/admin/admin-nav"; -import { CreditAdjustForm, PlanForm, RoleForm } from "@/components/admin/admin-actions"; -import { Card } from "@/components/ui/card"; +import { AdminUsersClient } from "./users-client"; import { isConfiguredAdminEmail } from "@/lib/admin-emails"; import { requireAdmin } from "@/lib/auth/current-user"; import { prisma } from "@/lib/db"; @@ -17,32 +15,26 @@ export default async function AdminUsersPage({ searchParams }: { searchParams: P prisma.user.findMany({ where, include: { subscription: { include: { plan: true } }, creditTransactions: { orderBy: { createdAt: "desc" }, take: 3 } }, orderBy: { createdAt: "desc" }, take: 100 }), prisma.plan.findMany({ where: { isActive: true }, orderBy: { sortOrder: "asc" } }) ]); - + const protectedAdminEmails: Record = {}; + for (const u of users) protectedAdminEmails[u.email] = isConfiguredAdminEmail(u.email); return ( -
-

Kullanıcı Yönetimi

Yetki, plan ve kredi bakiyelerini tek yerden yönetin.

- -
- - - -
-
- {users.map((user) => ( - -
-

{user.name || user.email}

{user.role}
{user.email} · {user.googleId ? "Google bağlı" : "E-posta hesabı"}
Son giriş: {user.lastLoginAt?.toLocaleString("tr-TR") || "—"} · Kayıt: {user.createdAt.toLocaleDateString("tr-TR")}
-
Plan
{user.subscription?.plan.code || "YOK"}
-
-
-
Yetki ve plan
plan.code)} />
- -
- {!!user.creditTransactions.length &&
{user.creditTransactions.map((tx) => {tx.amount > 0 ? "+" : ""}{tx.amount} · {tx.reason})}
} -
- ))} - {!users.length &&

Eşleşen kullanıcı bulunamadı.

} -
-
+ ({ + id: user.id, + name: user.name, + email: user.email, + googleId: user.googleId, + role: user.role, + lastLoginLabel: user.lastLoginAt?.toLocaleString("tr-TR") || "", + createdLabel: user.createdAt.toLocaleDateString("tr-TR"), + planCode: user.subscription?.plan.code || null, + creditBalance: user.creditBalance, + creditTransactions: user.creditTransactions.map((tx) => ({ id: tx.id, amount: tx.amount, reason: tx.reason })), + }))} + /> ); } diff --git a/src/app/dashboard/admin/users/users-client.tsx b/src/app/dashboard/admin/users/users-client.tsx new file mode 100644 index 0000000..d0c7d15 --- /dev/null +++ b/src/app/dashboard/admin/users/users-client.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { Prisma, UserRole } from "@prisma/client"; +import { AdminNav } from "@/components/admin/admin-nav"; +import { CreditAdjustForm, PlanForm, RoleForm } from "@/components/admin/admin-actions"; +import { Card } from "@/components/ui/card"; +import { useLang } from "@/lib/use-lang"; +import { useT } from "@/lib/i18n"; + +type PlanOption = { code: string }; +type TxOption = { id: string; amount: number; reason: string }; +type UserRow = { + id: string; + name: string | null; + email: string; + googleId: string | null; + role: string; + lastLoginLabel: string; + createdLabel: string; + planCode: string | null; + creditBalance: number; + creditTransactions: TxOption[]; +}; + +export function AdminUsersClient({ + users, + plans, + q, + role, + protectedAdminEmails, +}: { + users: UserRow[]; + plans: PlanOption[]; + q?: string; + role?: string; + protectedAdminEmails: Record; +}) { + const [lang] = useLang(); + const t = useT(); + void lang; + const locale = typeof document !== "undefined" && document.documentElement.lang === "en" ? "en-US" : "tr-TR"; + return ( +
+

{t("adminUsers.title")}

{t("adminUsers.subtitle")}

+ +
+ + + +
+
+ {users.map((user) => ( + +
+

{user.name || user.email}

{user.role}
{user.email} · {user.googleId ? t("adminUsers.googleLinked") : t("adminUsers.emailAccount")}
{t("adminUsers.lastLogin")}: {user.lastLoginLabel || "—"} · {t("adminUsers.registered")}: {user.createdLabel}
+
{t("adminUsers.plan")}
{user.planCode || t("adminUsers.none")}
+
+
+
{t("adminUsers.roleAndPlan")}
plan.code)} />
+ +
+ {!!user.creditTransactions.length &&
{user.creditTransactions.map((tx) => {tx.amount > 0 ? "+" : ""}{tx.amount} · {tx.reason})}
} +
+ ))} + {!users.length &&

{t("adminUsers.noMatch")}

} +
+
+ ); +} diff --git a/src/app/dashboard/magic-ai/magic-ai-client.tsx b/src/app/dashboard/magic-ai/magic-ai-client.tsx index 5995d49..66bdf01 100644 --- a/src/app/dashboard/magic-ai/magic-ai-client.tsx +++ b/src/app/dashboard/magic-ai/magic-ai-client.tsx @@ -4,14 +4,23 @@ import { AdCreativeMedia } from "@/components/ad-creative-media"; import { MagicAiRunner } from "@/components/magic-ai-runner"; import { useT } from "@/lib/i18n"; -export function MagicAiClient({ q, ads, analyses, sentence, recommendation }: { +export function MagicAiClient({ q, ads, analyses }: { q: string | undefined; ads: any[]; analyses: any[]; - sentence: (value?: string | null) => string; - recommendation: (ad: any) => { label: string; color: string; text: string }; }) { const t = useT(); + const sentence = (value?: string | null) => { + return value?.split(/[.!?\n]/).map((item) => item.trim()).find(Boolean) || t("ai.noHook"); + }; + const recommendation = (ad: { daysRunning: number | null; mediaType: string; primaryText: string | null; status: string }) => { + const days = ad.daysRunning || 0; + if (ad.status === "INACTIVE") return { label: t("ai.labelArchive"), color: "bg-slate-100 text-slate-700", text: t("ai.recArchive") }; + if (days >= 30) return { label: t("ai.labelScale"), color: "bg-emerald-50 text-emerald-700", text: t("ai.recScale", { days, media: ad.mediaType.toLowerCase() }) }; + if (days >= 14) return { label: t("ai.labelVariation"), color: "bg-violet-50 text-violet-700", text: t("ai.recVariation") }; + return { label: t("ai.labelWatch"), color: "bg-amber-50 text-amber-700", text: t("ai.recWatch") }; + }; + void q; return (

{t("ai.magic")}

{t("ai.description")}

@@ -23,4 +32,4 @@ export function MagicAiClient({ q, ads, analyses, sentence, recommendation }: { ; })}{!ads.length && {t("ai.noAdsToAnalyze")}}
); -} \ No newline at end of file +} diff --git a/src/app/dashboard/magic-ai/page.tsx b/src/app/dashboard/magic-ai/page.tsx index 181bfa9..e79d58c 100644 --- a/src/app/dashboard/magic-ai/page.tsx +++ b/src/app/dashboard/magic-ai/page.tsx @@ -3,18 +3,6 @@ import { prisma } from "@/lib/db"; import { requireUser } from "@/lib/auth/current-user"; import { MagicAiClient } from "./magic-ai-client"; -function sentence(value?: string | null) { - return value?.split(/[.!?\n]/).map((item) => item.trim()).find(Boolean) || "Metin kancası bulunamadı"; -} - -function recommendation(ad: { daysRunning: number | null; mediaType: string; primaryText: string | null; status: string }) { - const days = ad.daysRunning || 0; - if (ad.status === "INACTIVE") return { label: "ARŞİV", color: "bg-slate-100 text-slate-700", text: "Reklam artık aktif değil; mesajı referans olarak sakla, doğrudan ölçekleme yapma." }; - if (days >= 30) return { label: "ÖLÇEKLE", color: "bg-emerald-50 text-emerald-700", text: `${days} günlük dayanıklılık güçlü bir pazar uyumuna işaret ediyor. Kancayı yeni ${ad.mediaType.toLowerCase()} varyasyonlarında test et.` }; - if (days >= 14) return { label: "VARYASYON", color: "bg-violet-50 text-violet-700", text: "Kreatif yeterli süre yaşamış. Aynı vaadi farklı açılış karesi ve CTA ile çoğalt." }; - return { label: "İZLE", color: "bg-amber-50 text-amber-700", text: "Reklam yeni. Harcama sinyali oluşmadan önce birkaç gün daha performansını izle." }; -} - export default async function MagicAIPage({ searchParams }: { searchParams: Promise> }) { const user = await requireUser(); const q = (await searchParams).q?.trim(); @@ -24,5 +12,5 @@ export default async function MagicAIPage({ searchParams }: { searchParams: Prom prisma.aiAnalysis.findMany({ where: { userId: user.id }, orderBy: { createdAt: "desc" }, take: 5 }) ]); - return ; + return ; } diff --git a/src/app/dashboard/stores/[id]/page.tsx b/src/app/dashboard/stores/[id]/page.tsx index 3754da4..ea3ce19 100644 --- a/src/app/dashboard/stores/[id]/page.tsx +++ b/src/app/dashboard/stores/[id]/page.tsx @@ -1,8 +1,7 @@ import { notFound } from "next/navigation"; import { prisma } from "@/lib/db"; import { requireUser } from "@/lib/auth/current-user"; -import { Card } from "@/components/ui/card"; -import { AdCreativeMedia } from "@/components/ad-creative-media"; +import { StoreDetailClient } from "./store-detail-client"; export default async function StoreDetailPage({ params }: { params: Promise<{ id: string }> }) { const resolvedParams = await params; @@ -15,46 +14,25 @@ export default async function StoreDetailPage({ params }: { params: Promise<{ id const similar = await prisma.store.findMany({ where: { id: { not: store.id }, niche: store.niche }, take: 8, orderBy: { monthlyVisits: "desc" } }); const ads = store.brandPages.flatMap((p) => p.ads); return ( -
-
-
- -
-

{store.name}

-

{store.domain} · {store.country} · {store.niche}

-
-
- Visit Shop -
-
-
Monthly Visits
{store.monthlyVisits?.toLocaleString()}
-
Est Revenue
€{store.estRevenue30dMin?.toLocaleString()}–€{store.estRevenue30dMax?.toLocaleString()}
-
Products
{store.productCount}
-
Active Ads
{ads.length}
-
-
- -

Best-selling Products

-
- {store.products.map((p) =>
{p.title}
{p.currency} {p.price}
)} -
-
- -

Pixels & Stack

-
{store.pixels.map((p) => {p.type})}
-
{store.apps.map((a) => {a.name})}
-
-
- -

Top Meta Ads

-
- {ads.map((ad) =>
{ad.headline}
Top %{ad.rankPercentile} · {ad.daysRunning} gün
)} -
-
- -

Similar Stores

- -
-
+ ({ id: ad.id, headline: ad.headline, rankPercentile: ad.rankPercentile, daysRunning: ad.daysRunning, creative: ad.creatives[0] ?? null }))} + products={store.products.map((p) => ({ id: p.id, title: p.title, currency: p.currency, price: p.price }))} + pixels={store.pixels.map((p) => ({ id: p.id, type: p.type }))} + apps={store.apps.map((a) => ({ id: a.id, name: a.name }))} + similar={similar.map((s) => ({ id: s.id, name: s.name, visitsLabel: s.monthlyVisits?.toLocaleString() ?? "—" }))} + /> ); } diff --git a/src/app/dashboard/stores/[id]/store-detail-client.tsx b/src/app/dashboard/stores/[id]/store-detail-client.tsx new file mode 100644 index 0000000..e3fa86f --- /dev/null +++ b/src/app/dashboard/stores/[id]/store-detail-client.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { Card } from "@/components/ui/card"; +import { AdCreativeMedia } from "@/components/ad-creative-media"; +import { useLang } from "@/lib/use-lang"; +import { useT } from "@/lib/i18n"; + +export type CreativeLite = Parameters[0]["creative"]; +export type AdLite = { id: string; headline: string | null; rankPercentile: number | null; daysRunning: number | null; creative: CreativeLite }; +export type ProductLite = { id: string; title: string; currency: string | null; price: number | null }; +export type PixelLite = { id: string; type: string }; +export type AppLite = { id: string; name: string }; +export type SimilarLite = { id: string; name: string; visitsLabel: string }; + +export function StoreDetailClient({ + store, + adsCount, + ads, + products, + pixels, + apps, + similar, +}: { + store: { name: string; domain: string | null; country: string | null; niche: string | null; logoUrl: string | null; shopUrl: string | null; monthlyVisitsLabel: string; revenueMinLabel: string; revenueMaxLabel: string; productCount: number }; + adsCount: number; + ads: AdLite[]; + products: ProductLite[]; + pixels: PixelLite[]; + apps: AppLite[]; + similar: SimilarLite[]; +}) { + const [lang] = useLang(); + const t = useT(); + void lang; + return ( +
+
+
+ +
+

{store.name}

+

{store.domain} · {store.country} · {store.niche}

+
+
+ {t("storeDetail.visitShop")} +
+
+
{t("storeDetail.monthlyVisits")}
{store.monthlyVisitsLabel}
+
{t("storeDetail.estRevenue")}
€{store.revenueMinLabel}–€{store.revenueMaxLabel}
+
{t("storeDetail.products")}
{store.productCount}
+
{t("storeDetail.activeAds")}
{adsCount}
+
+
+ +

{t("storeDetail.bestSelling")}

+
+ {products.map((p) =>
{p.title}
{p.currency} {p.price}
)} +
+
+ +

{t("storeDetail.pixelsStack")}

+
{pixels.map((p) => {p.type})}
+
{apps.map((a) => {a.name})}
+
+
+ +

{t("storeDetail.topMetaAds")}

+
+ {ads.map((ad) =>
{ad.headline}
{t("storeDetail.topPercent", { percentile: ad.rankPercentile ?? 0 })} · {t("storeDetail.daysRunning", { days: ad.daysRunning ?? 0 })}
)} +
+
+ +

{t("storeDetail.similarStores")}

+ +
+
+ ); +} diff --git a/src/components/admin/admin-actions.tsx b/src/components/admin/admin-actions.tsx index 7983f16..57deb93 100644 --- a/src/components/admin/admin-actions.tsx +++ b/src/components/admin/admin-actions.tsx @@ -2,11 +2,12 @@ import { FormEvent, useState } from "react"; import { useRouter } from "next/navigation"; +import { useT } from "@/lib/i18n"; async function postJson(url: string, body: unknown) { const response = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); const data = await response.json().catch(() => ({})); - if (!response.ok) throw new Error(data.error || "İşlem başarısız"); + if (!response.ok) throw new Error((data as { error?: string }).error || "FAIL"); return data; } @@ -17,6 +18,7 @@ function Feedback({ message, error }: { message: string; error: boolean }) { export function CreditAdjustForm({ userId, currentBalance }: { userId: string; currentBalance: number }) { const router = useRouter(); + const t = useT(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); @@ -27,23 +29,23 @@ export function CreditAdjustForm({ userId, currentBalance }: { userId: string; c const form = new FormData(formElement); const operation = String(form.get("operation")); const amount = Number(form.get("amount")); - if (operation === "remove" && !window.confirm(`${amount} kredi bakiyeden çıkarılsın mı?`)) return; + if (operation === "remove" && !window.confirm(t("credit.confirmRemove", { amount }))) return; setBusy(true); setMessage(""); try { const result = await postJson(`/api/admin/users/${userId}/credits`, { operation, amount, reason: form.get("reason") }); - setIsError(false); setMessage(`Yeni bakiye: ${result.balance}`); formElement.reset(); router.refresh(); - } catch (error) { setIsError(true); setMessage(error instanceof Error ? error.message : "İşlem başarısız"); } + setIsError(false); setMessage(t("credit.newBalance", { balance: (result as { balance: number }).balance })); formElement.reset(); router.refresh(); + } catch (error) { setIsError(true); setMessage(error instanceof Error && error.message !== "FAIL" ? error.message : t("common.operationFailed")); } finally { setBusy(false); } } return (
-
Kredi bakiyesi: {currentBalance.toLocaleString("tr-TR")}
+
{t("credit.balance")}: {currentBalance.toLocaleString("tr-TR")}
- - - - + + + +
@@ -52,76 +54,77 @@ export function CreditAdjustForm({ userId, currentBalance }: { userId: string; c export function RoleForm({ userId, currentRole, protectedAdmin }: { userId: string; currentRole: "USER" | "ADMIN"; protectedAdmin: boolean }) { const router = useRouter(); + const t = useT(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); const form = new FormData(event.currentTarget); const role = String(form.get("role")); - if (role === "USER" && !window.confirm("Bu kullanıcının admin yetkisi kaldırılsın mı?")) return; + if (role === "USER" && !window.confirm(t("role.confirmDemote"))) return; setBusy(true); setMessage(""); - try { await postJson(`/api/admin/users/${userId}/role`, { role }); setIsError(false); setMessage("Yetki güncellendi"); router.refresh(); } - catch (error) { setIsError(true); setMessage(error instanceof Error ? error.message : "İşlem başarısız"); } + try { await postJson(`/api/admin/users/${userId}/role`, { role }); setIsError(false); setMessage(t("role.updated")); router.refresh(); } + catch (error) { setIsError(true); setMessage(error instanceof Error && error.message !== "FAIL" ? error.message : t("common.operationFailed")); } finally { setBusy(false); } } return (
- - {protectedAdmin && Korumalı admin} + + {protectedAdmin && {t("role.protected")}} ); } export function PlanForm({ userId, currentPlan, plans }: { userId: string; currentPlan: string; plans: string[] }) { - const router = useRouter(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); + const router = useRouter(); const t = useT(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); const form = new FormData(event.currentTarget); setBusy(true); setMessage(""); - try { await postJson(`/api/admin/users/${userId}/plan`, { planCode: form.get("planCode"), status: form.get("status") }); setIsError(false); setMessage("Plan güncellendi"); router.refresh(); } - catch (error) { setIsError(true); setMessage(error instanceof Error ? error.message : "İşlem başarısız"); } + try { await postJson(`/api/admin/users/${userId}/plan`, { planCode: form.get("planCode"), status: form.get("status") }); setIsError(false); setMessage(t("plan.updated")); router.refresh(); } + catch (error) { setIsError(true); setMessage(error instanceof Error && error.message !== "FAIL" ? error.message : t("common.operationFailed")); } finally { setBusy(false); } } return (
- + ); } export function ManualPaymentForm({ users }: { users: Array<{ id: string; email: string }> }) { - const router = useRouter(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); + const router = useRouter(); const t = useT(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [isError, setIsError] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); const formElement = event.currentTarget; const form = new FormData(formElement); setBusy(true); setMessage(""); try { await postJson("/api/admin/payments", { userId: form.get("userId"), amountCents: Math.round(Number(form.get("amount")) * 100), currency: form.get("currency"), creditGranted: Number(form.get("creditGranted") || 0), description: form.get("description"), externalId: form.get("externalId") || null }); - setIsError(false); setMessage("Ödeme kaydedildi"); formElement.reset(); router.refresh(); - } catch (error) { setIsError(true); setMessage(error instanceof Error ? error.message : "İşlem başarısız"); } + setIsError(false); setMessage(t("payment.saved")); formElement.reset(); router.refresh(); + } catch (error) { setIsError(true); setMessage(error instanceof Error && error.message !== "FAIL" ? error.message : t("common.operationFailed")); } finally { setBusy(false); } } return (
- -
- - - - + +
+ + + +
); } export function PaymentStatusForm({ paymentId, status }: { paymentId: string; status: string }) { - const router = useRouter(); const [busy, setBusy] = useState(false); + const router = useRouter(); const t = useT(); const [busy, setBusy] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); const form = new FormData(event.currentTarget); const nextStatus = String(form.get("status")); - if ((nextStatus === "REFUNDED" || nextStatus === "CANCELED") && !window.confirm(`Ödeme ${nextStatus} yapılsın mı? Kredi bakiyesi otomatik değişmez.`)) return; + if ((nextStatus === "REFUNDED" || nextStatus === "CANCELED") && !window.confirm(t("payment.confirmStatus", { status: nextStatus }))) return; setBusy(true); try { await postJson(`/api/admin/payments/${paymentId}/status`, { status: nextStatus }); router.refresh(); } finally { setBusy(false); } } - return
; + return
; } diff --git a/src/components/admin/admin-nav.tsx b/src/components/admin/admin-nav.tsx index fdca403..6dc2097 100644 --- a/src/components/admin/admin-nav.tsx +++ b/src/components/admin/admin-nav.tsx @@ -1,19 +1,23 @@ -import Link from "next/link"; +"use client"; -const links = [ - ["Genel Bakış", "/dashboard/admin"], - ["Kullanıcılar", "/dashboard/admin/users"], - ["Ödemeler", "/dashboard/admin/payments"], - ["Log Merkezi", "/dashboard/admin/logs"], - ["Veri İşleri", "/dashboard/admin/data"] +import Link from "next/link"; +import { useT } from "@/lib/i18n"; + +const links: Array<[string, string]> = [ + ["adminNav.overview", "/dashboard/admin"], + ["adminNav.users", "/dashboard/admin/users"], + ["adminNav.payments", "/dashboard/admin/payments"], + ["adminNav.logs", "/dashboard/admin/logs"], + ["adminNav.data", "/dashboard/admin/data"] ]; export function AdminNav() { + const t = useT(); return (
- {links.map(([label, href]) => ( + {links.map(([key, href]) => ( - {label} + {t(key)} ))}
diff --git a/src/components/admin/apify-ingest-form.tsx b/src/components/admin/apify-ingest-form.tsx index e4fb281..f24a216 100644 --- a/src/components/admin/apify-ingest-form.tsx +++ b/src/components/admin/apify-ingest-form.tsx @@ -2,9 +2,11 @@ import { FormEvent, useState } from "react"; import { useRouter } from "next/navigation"; +import { useT } from "@/lib/i18n"; export function ApifyIngestForm({ configured }: { configured: boolean }) { const router = useRouter(); + const t = useT(); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); const [error, setError] = useState(false); @@ -15,7 +17,7 @@ export function ApifyIngestForm({ configured }: { configured: boolean }) { const searchTerms = String(form.get("searchTerms") || "").split(",").map((term) => term.trim()).filter(Boolean); const maxResults = Number(form.get("maxResults")); const maxCostUsd = Number(form.get("maxCostUsd")); - if (!window.confirm(`${searchTerms.join(", ")} için en fazla ${maxResults} reklam çekilsin mi? Harcama üst sınırı $${maxCostUsd.toFixed(2)}.`)) return; + if (!window.confirm(t("ingest.confirm", { terms: searchTerms.join(", "), max: maxResults, cost: maxCostUsd.toFixed(2) }))) return; setBusy(true); setMessage(""); try { const response = await fetch("/api/admin/ingest/apify", { @@ -23,16 +25,16 @@ export function ApifyIngestForm({ configured }: { configured: boolean }) { body: JSON.stringify({ searchTerms, country: form.get("country"), adActiveStatus: form.get("adActiveStatus"), mediaType: form.get("mediaType"), maxResults, maxCostUsd, scrapeAdDetails: form.get("scrapeAdDetails") === "on", includeAboutPage: form.get("includeAboutPage") === "on" }) }); const data = await response.json().catch(() => ({})); - if (!response.ok) throw new Error("Canlı reklam taraması başarısız oldu."); - setError(false); setMessage(`${data.imported} reklam işlendi, ${data.failed} kayıt atlandı.`); router.refresh(); + if (!response.ok) throw new Error(t("ingest.scanFailed")); + setError(false); setMessage(t("ingest.done", { imported: data.imported, failed: data.failed })); router.refresh(); } catch (caught) { - setError(true); setMessage(caught instanceof Error ? caught.message : "İşlem başarısız"); + setError(true); setMessage(caught instanceof Error && caught.message !== t("ingest.scanFailed") ? caught.message : t("ingest.failed")); } finally { setBusy(false); } } return (
- +
@@ -41,10 +43,10 @@ export function ApifyIngestForm({ configured }: { configured: boolean }) {
- - - - + + + + {message &&

{message}

}
); diff --git a/src/components/ads/apify-empty-search.tsx b/src/components/ads/apify-empty-search.tsx index d6a087b..2b7e025 100644 --- a/src/components/ads/apify-empty-search.tsx +++ b/src/components/ads/apify-empty-search.tsx @@ -2,12 +2,14 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; +import { useT } from "@/lib/i18n"; const RESULT_LIMIT = 10; const RESULT_OPTIONS = [10, 25, 50, 100]; export function ApifyEmptySearch({ query, country, mediaType, matchMode, status, planLimit, existingCount = 0 }: { query: string; country: string; mediaType: string; matchMode: "ALL_WORDS" | "EXACT_PHRASE"; status: "ACTIVE" | "INACTIVE" | "ALL"; planLimit: number; existingCount?: number }) { const router = useRouter(); + const t = useT(); const [maxResults, setMaxResults] = useState(RESULT_LIMIT); const [busy, setBusy] = useState(false); const [message, setMessage] = useState(""); @@ -38,17 +40,17 @@ export function ApifyEmptySearch({ query, country, mediaType, matchMode, status, }) }); const data = await response.json().catch(() => ({})); - if (!response.ok) throw new Error("Canlı reklam taraması başarısız oldu."); + if (!response.ok) throw new Error(t("apifyEmpty.scanFailed")); setError(false); const prepared = data.mediaEnriched ?? data.imported ?? 0; setMessage(prepared > 0 - ? `Meta'da ${data.received} aday tarandı, ${data.relevant} ilgili reklam bulundu; ${prepared} reklamın medyası hazırlandı. Sonuçlar yenileniyor…` - : `Meta'da ${data.received} aday ve ${data.relevant} ilgili reklam bulundu; seçili ${mediaType === "ALL" ? "medya" : mediaType.toLocaleLowerCase("tr-TR")} türünde kullanılabilir kreatif bulunamadı.`); + ? t("apifyEmpty.successPrepared", { received: data.received, relevant: data.relevant, prepared }) + : t("apifyEmpty.successNoCreative", { received: data.received, relevant: data.relevant })); router.refresh(); } catch (caught) { setError(true); - setMessage(caught instanceof Error ? caught.message : "Canlı reklam taraması başarısız oldu."); + setMessage(caught instanceof Error && caught.message !== t("apifyEmpty.scanFailed") ? caught.message : t("apifyEmpty.scanFailed")); } finally { setBusy(false); } @@ -56,14 +58,14 @@ export function ApifyEmptySearch({ query, country, mediaType, matchMode, status, return (
-

{existingCount > 0 ? `Daha fazla “${query}” reklamı getir` : `“${query}” için kayıtlı reklam bulunamadı`}

+

{existingCount > 0 ? t("apifyEmpty.bringMore", { query }) : t("apifyEmpty.noneFound", { query })}

{existingCount > 0 - ? `Şu anda ${existingCount} ilgili reklam gösteriliyor. Yeni Meta reklamlarını aynı detaylı filtrelerle canlı olarak tarayabilirsiniz.` - : "Bu arama önce AdSeeQ veritabanını kontrol eder. Yeni Meta reklamlarını seçtiğiniz ülke ve medya türüyle canlı olarak getirip aynı aramaya ekleyebilirsiniz."} + ? t("apifyEmpty.showingExisting", { count: existingCount }) + : t("apifyEmpty.dbFirst")}

- +
-

Canlı Meta verisi · {country === "ALL" ? "Tüm dünya" : country} · {mediaType === "ALL" ? "Tüm medya" : mediaType} · {matchMode === "EXACT_PHRASE" ? "Tam ifade" : "Tüm kelimeler"} · Plan limiti: {planLimit || "erişim yok"} reklam

- {busy &&

Meta sonuçları ve gerçek video/görseller hazırlanıyor. Bu işlem genellikle 15–60 saniye sürer.

} +

{t("apifyEmpty.liveMeta")} · {country === "ALL" ? t("apifyEmpty.allWorld") : country} · {mediaType === "ALL" ? t("apifyEmpty.allMedia") : mediaType} · {matchMode === "EXACT_PHRASE" ? t("apifyEmpty.exactPhrase") : t("apifyEmpty.allWords")} · {t("apifyEmpty.planLimit")}: {planLimit || t("apifyEmpty.noAccess")}

+ {busy &&

{t("apifyEmpty.busyNote")}

} {message &&

{message}

}
); diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 3328a43..2777e7e 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -198,6 +198,147 @@ const DICT: Dict = { // ─── Admin ─── "admin.title": { tr: "AdSeeQ Admin", en: "AdSeeQ Admin" }, + "adminUsers.title": { tr: "Kullanıcı Yönetimi", en: "User Management" }, + "adminUsers.subtitle": { tr: "Yetki, plan ve kredi bakiyelerini tek yerden yönetin.", en: "Manage roles, plans and credit balances in one place." }, + "adminUsers.searchPlaceholder": { tr: "E-posta veya ad ara", en: "Search by email or name" }, + "adminUsers.allRoles": { tr: "Tüm yetkiler", en: "All roles" }, + "adminUsers.filter": { tr: "Filtrele", en: "Filter" }, + "adminUsers.googleLinked": { tr: "Google bağlı", en: "Google linked" }, + "adminUsers.emailAccount": { tr: "E-posta hesabı", en: "Email account" }, + "adminUsers.lastLogin": { tr: "Son giriş", en: "Last login" }, + "adminUsers.registered": { tr: "Kayıt", en: "Registered" }, + "adminUsers.plan": { tr: "Plan", en: "Plan" }, + "adminUsers.none": { tr: "YOK", en: "NONE" }, + "adminUsers.roleAndPlan": { tr: "Yetki ve plan", en: "Role and plan" }, + "adminUsers.noMatch": { tr: "Eşleşen kullanıcı bulunamadı.", en: "No matching users found." }, + "adminPayments.title": { tr: "Ödemeler", en: "Payments" }, + "adminPayments.subtitle": { tr: "Manuel tahsilat, referans ve ödeme durumlarını yönetin.", en: "Manage manual collections, references and payment statuses." }, + "adminPayments.addManual": { tr: "Manuel ödeme ekle", en: "Add manual payment" }, + "adminPayments.atomicNote": { tr: "Ödeme ile verilen kredi atomik olarak bakiyeye eklenir. İade durumunda kredi otomatik düşmez; kullanıcı ekranından gerekçeli işlem yapın.", en: "Credits granted with a payment are added to the balance atomically. On refund, credits are not deducted automatically; process it with a reason from the user screen." }, + "adminPayments.user": { tr: "Kullanıcı", en: "User" }, + "adminPayments.amount": { tr: "Tutar", en: "Amount" }, + "adminPayments.provider": { tr: "Sağlayıcı", en: "Provider" }, + "adminPayments.credit": { tr: "Kredi", en: "Credit" }, + "adminPayments.reference": { tr: "Referans", en: "Reference" }, + "adminPayments.date": { tr: "Tarih", en: "Date" }, + "adminPayments.status": { tr: "Durum", en: "Status" }, + "adminPayments.adminLabel": { tr: "Admin", en: "Admin" }, + "adminPayments.system": { tr: "Sistem", en: "System" }, + "adminPayments.noRecords": { tr: "Henüz ödeme kaydı yok.", en: "No payment records yet." }, + "adminLogs.title": { tr: "Log Merkezi", en: "Log Center" }, + "adminLogs.subtitle": { tr: "Admin audit, kredi hareketleri, veri işleri ve oturum kayıtları.", en: "Admin audit logs, credit transactions, data jobs and session records." }, + "adminLogs.auditTitle": { tr: "Admin audit logları", en: "Admin audit logs" }, + "adminLogs.noAudit": { tr: "Audit kaydı yok.", en: "No audit records." }, + "adminLogs.creditsTitle": { tr: "Kredi hareketleri", en: "Credit transactions" }, + "adminLogs.user": { tr: "Kullanıcı", en: "User" }, + "adminLogs.type": { tr: "Tür", en: "Type" }, + "adminLogs.amount": { tr: "Miktar", en: "Amount" }, + "adminLogs.balance": { tr: "Bakiye", en: "Balance" }, + "adminLogs.reason": { tr: "Neden", en: "Reason" }, + "adminLogs.date": { tr: "Tarih", en: "Date" }, + "adminLogs.ingestTitle": { tr: "Ingest işleri", en: "Ingest jobs" }, + "adminLogs.ok": { tr: "başarılı", en: "succeeded" }, + "adminLogs.failed": { tr: "hatalı", en: "failed" }, + "adminLogs.exportTitle": { tr: "Export işleri", en: "Export jobs" }, + "adminLogs.sessionsTitle": { tr: "Son oturumlar", en: "Recent sessions" }, + "adminLogs.opened": { tr: "Açılış", en: "Opened" }, + "adminLogs.expires": { tr: "Bitiş", en: "Expires" }, + "adminData.title": { tr: "Veri İşleri", en: "Data Operations" }, + "adminData.subtitle": { tr: "Reklam, mağaza ve ingest operasyonları.", en: "Ad, store and ingest operations." }, + "adminData.ads": { tr: "Reklamlar", en: "Ads" }, + "adminData.stores": { tr: "Mağazalar", en: "Stores" }, + "adminData.users": { tr: "Kullanıcılar", en: "Users" }, + "adminData.metaAdsTitle": { tr: "Meta Ads Library", en: "Meta Ads Library" }, + "adminData.metaAdsDesc": { tr: "Anahtar kelimeyle gerçek Meta reklamlarını çekin. Her çalışmada maliyet üst sınırı uygulanır ve tekrar eden reklamlar güncellenir.", en: "Pull real Meta ads by keyword. Each run enforces a cost cap and upserts duplicate ads." }, + "adminData.ingestJobs": { tr: "Ingest İşleri", en: "Ingest Jobs" }, + "adminData.ok": { tr: "başarılı", en: "succeeded" }, + "adminData.failed": { tr: "hatalı", en: "failed" }, + "adminData.noJobs": { tr: "Henüz ingest işi yok.", en: "No ingest jobs yet." }, + "storeDetail.visitShop": { tr: "Mağazayı Ziyaret Et", en: "Visit Shop" }, + "storeDetail.monthlyVisits": { tr: "Aylık Ziyaret", en: "Monthly Visits" }, + "storeDetail.estRevenue": { tr: "Tahmini Gelir", en: "Est Revenue" }, + "storeDetail.products": { tr: "Ürünler", en: "Products" }, + "storeDetail.activeAds": { tr: "Aktif Reklam", en: "Active Ads" }, + "storeDetail.bestSelling": { tr: "En Çok Satan Ürünler", en: "Best-selling Products" }, + "storeDetail.pixelsStack": { tr: "Pixel & Yazılım", en: "Pixels & Stack" }, + "storeDetail.topMetaAds": { tr: "Öne Çıkan Meta Reklamları", en: "Top Meta Ads" }, + "storeDetail.similarStores": { tr: "Benzer Mağazalar", en: "Similar Stores" }, + "storeDetail.visits": { tr: "ziyaret", en: "visits" }, + "storeDetail.topPercent": { tr: "Top %{{percentile}}", en: "Top {{percentile}}%" }, + "storeDetail.daysRunning": { tr: "{{days}} gün", en: "{{days}} days" }, + "ai.noHook": { tr: "Metin kancası bulunamadı", en: "No text hook found" }, + "ai.labelArchive": { tr: "ARŞİV", en: "ARCHIVE" }, + "ai.recArchive": { tr: "Reklam artık aktif değil; mesajı referans olarak sakla, doğrudan ölçekleme yapma.", en: "The ad is no longer active; keep the message as a reference, do not scale directly." }, + "ai.labelScale": { tr: "ÖLÇEKLE", en: "SCALE" }, + "ai.recScale": { tr: "{{days}} günlük dayanıklılık güçlü bir pazar uyumuna işaret ediyor. Kancayı yeni {{media}} varyasyonlarında test et.", en: "{{days}} days of durability signals strong market fit. Test the hook in new {{media}} variations." }, + "ai.labelVariation": { tr: "VARYASYON", en: "VARIATION" }, + "ai.recVariation": { tr: "Kreatif yeterli süre yaşamış. Aynı vaadi farklı açılış karesi ve CTA ile çoğalt.", en: "The creative has lived long enough. Replicate the same promise with a different opening frame and CTA." }, + "ai.labelWatch": { tr: "İZLE", en: "WATCH" }, + "ai.recWatch": { tr: "Reklam yeni. Harcama sinyali oluşmadan önce birkaç gün daha performansını izle.", en: "The ad is new. Watch its performance for a few more days before a spend signal forms." }, + "adminNav.overview": { tr: "Genel Bakış", en: "Overview" }, + "adminNav.users": { tr: "Kullanıcılar", en: "Users" }, + "adminNav.payments": { tr: "Ödemeler", en: "Payments" }, + "adminNav.logs": { tr: "Log Merkezi", en: "Log Center" }, + "adminNav.data": { tr: "Veri İşleri", en: "Data Ops" }, + "common.operationFailed": { tr: "İşlem başarısız", en: "Operation failed" }, + "common.apply": { tr: "Uygula", en: "Apply" }, + "common.save": { tr: "Kaydet", en: "Save" }, + "common.saving": { tr: "Kaydediliyor...", en: "Saving..." }, + "credit.confirmRemove": { tr: "{{amount}} kredi bakiyeden çıkarılsın mı?", en: "Remove {{amount}} credits from the balance?" }, + "credit.newBalance": { tr: "Yeni bakiye: {{balance}}", en: "New balance: {{balance}}" }, + "credit.balance": { tr: "Kredi bakiyesi", en: "Credit balance" }, + "credit.add": { tr: "Kredi ekle", en: "Add credits" }, + "credit.remove": { tr: "Kredi çıkar", en: "Remove credits" }, + "credit.amount": { tr: "Miktar", en: "Amount" }, + "credit.reason": { tr: "İşlem nedeni", en: "Reason" }, + "role.confirmDemote": { tr: "Bu kullanıcının admin yetkisi kaldırılsın mı?", en: "Remove admin privileges from this user?" }, + "role.updated": { tr: "Yetki güncellendi", en: "Role updated" }, + "role.assign": { tr: "Yetki ata", en: "Assign role" }, + "role.protected": { tr: "Korumalı admin", en: "Protected admin" }, + "plan.updated": { tr: "Plan güncellendi", en: "Plan updated" }, + "plan.save": { tr: "Planı kaydet", en: "Save plan" }, + "payment.saved": { tr: "Ödeme kaydedildi", en: "Payment saved" }, + "payment.selectUser": { tr: "Kullanıcı seçin", en: "Select user" }, + "payment.amount": { tr: "Ödeme tutarı", en: "Amount" }, + "payment.credit": { tr: "Verilecek kredi", en: "Credits to grant" }, + "payment.reference": { tr: "Dekont / referans no (opsiyonel)", en: "Receipt / reference no (optional)" }, + "payment.description": { tr: "Açıklama", en: "Description" }, + "payment.manualDefault": { tr: "Manuel ödeme", en: "Manual payment" }, + "payment.save": { tr: "Ödemeyi kaydet", en: "Save payment" }, + "payment.confirmStatus": { tr: "Ödeme {{status}} yapılsın mı? Kredi bakiyesi otomatik değişmez.", en: "Mark payment as {{status}}? Credit balance does not change automatically." }, + "apifyEmpty.scanFailed": { tr: "Canlı reklam taraması başarısız oldu.", en: "Live ad scan failed." }, + "apifyEmpty.successPrepared": { tr: "Meta'da {{received}} aday tarandı, {{relevant}} ilgili reklam bulundu; {{prepared}} reklamın medyası hazırlandı. Sonuçlar yenileniyor…", en: "Scanned {{received}} candidates on Meta, found {{relevant}} relevant ads; media prepared for {{prepared}}. Refreshing results…" }, + "apifyEmpty.successNoCreative": { tr: "Meta'da {{received}} aday ve {{relevant}} ilgili reklam bulundu; seçili medya türünde kullanılabilir kreatif bulunamadı.", en: "Found {{received}} candidates and {{relevant}} relevant ads on Meta; no creatives available for the selected media type." }, + "apifyEmpty.bringMore": { tr: "Daha fazla \"{{query}}\" reklamı getir", en: "Bring more \"{{query}}\" ads" }, + "apifyEmpty.noneFound": { tr: "\"{{query}}\" için kayıtlı reklam bulunamadı", en: "No saved ads found for \"{{query}}\"" }, + "apifyEmpty.showingExisting": { tr: "Şu anda {{count}} ilgili reklam gösteriliyor. Yeni Meta reklamlarını aynı detaylı filtrelerle canlı olarak tarayabilirsiniz.", en: "Currently showing {{count}} relevant ads. You can scan new Meta ads live with the same detailed filters." }, + "apifyEmpty.dbFirst": { tr: "Bu arama önce AdSeeQ veritabanını kontrol eder. Yeni Meta reklamlarını seçtiğiniz ülke ve medya türüyle canlı olarak getirip aynı aramaya ekleyebilirsiniz.", en: "This search checks the AdSeeQ database first. You can pull new Meta ads live with your selected country and media type and add them to the same search." }, + "apifyEmpty.resultCount": { tr: "Getirilecek reklam adedi", en: "Number of ads to fetch" }, + "apifyEmpty.adsCount": { tr: "{{count}} reklam", en: "{{count}} ads" }, + "apifyEmpty.preparingVideos": { tr: "Videolar hazırlanıyor…", en: "Preparing videos…" }, + "apifyEmpty.scanningMeta": { tr: "Meta taranıyor…", en: "Scanning Meta…" }, + "apifyEmpty.upgradePlan": { tr: "Planı yükselt", en: "Upgrade plan" }, + "apifyEmpty.bringMoreBtn": { tr: "Daha fazla getir", en: "Bring more" }, + "apifyEmpty.bring": { tr: "Getir", en: "Fetch" }, + "apifyEmpty.liveMeta": { tr: "Canlı Meta verisi", en: "Live Meta data" }, + "apifyEmpty.allWorld": { tr: "Tüm dünya", en: "Worldwide" }, + "apifyEmpty.allMedia": { tr: "Tüm medya", en: "All media" }, + "apifyEmpty.exactPhrase": { tr: "Tam ifade", en: "Exact phrase" }, + "apifyEmpty.allWords": { tr: "Tüm kelimeler", en: "All words" }, + "apifyEmpty.planLimit": { tr: "Plan limiti", en: "Plan limit" }, + "apifyEmpty.noAccess": { tr: "erişim yok", en: "no access" }, + "apifyEmpty.busyNote": { tr: "Meta sonuçları ve gerçek video/görseller hazırlanıyor. Bu işlem genellikle 15–60 saniye sürer.", en: "Preparing Meta results and real videos/images. This usually takes 15–60 seconds." }, + "ingest.confirm": { tr: "{{terms}} için en fazla {{max}} reklam çekilsin mi? Harcama üst sınırı ${{cost}}.", en: "Pull up to {{max}} ads for {{terms}}? Spend cap ${{cost}}." }, + "ingest.scanFailed": { tr: "Canlı reklam taraması başarısız oldu.", en: "Live ad scan failed." }, + "ingest.done": { tr: "{{imported}} reklam işlendi, {{failed}} kayıt atlandı.", en: "{{imported}} ads processed, {{failed}} records skipped." }, + "ingest.failed": { tr: "İşlem başarısız", en: "Operation failed" }, + "ingest.termsPlaceholder": { tr: "Anahtar kelimeler: skincare, dog collar", en: "Keywords: skincare, dog collar" }, + "ingest.scrapeDetails": { tr: "Yaratıcı/CTA detaylarını al", en: "Scrape creative/CTA details" }, + "ingest.aboutPage": { tr: "Reklamveren detaylarını al", en: "Scrape advertiser details" }, + "ingest.costLimit": { tr: "Harcama üst sınırı (USD)", en: "Spend cap (USD)" }, + "ingest.waitingConfig": { tr: "Veri kaynağı ayarı bekleniyor", en: "Waiting for data source config" }, + "ingest.scanning": { tr: "Veriler taranıyor...", en: "Scanning..." }, + "ingest.pull": { tr: "Reklamları çek", en: "Pull ads" }, "admin.controlCenter": { tr: "Kontrol Merkezi", en: "Control Center" }, "admin.description": { tr: "Kullanıcı, gelir, kredi ve operasyon görünümü.", en: "User, revenue, credit and operations overview." }, "admin.systemActive": { tr: "Sistem aktif", en: "System active" }, @@ -251,7 +392,6 @@ const DICT: Dict = { "common.loading": { tr: "Yükleniyor...", en: "Loading..." }, "common.search": { tr: "Ara", en: "Search" }, "common.filter": { tr: "Filtrele", en: "Filter" }, - "common.save": { tr: "Kaydet", en: "Save" }, "common.cancel": { tr: "İptal", en: "Cancel" }, "common.delete": { tr: "Sil", en: "Delete" }, "common.edit": { tr: "Düzenle", en: "Edit" },