diff --git a/package-lock.json b/package-lock.json
index fbeb152..a842db6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1732,7 +1732,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
diff --git a/src/app/dashboard/account/account-client.tsx b/src/app/dashboard/account/account-client.tsx
new file mode 100644
index 0000000..238a3e1
--- /dev/null
+++ b/src/app/dashboard/account/account-client.tsx
@@ -0,0 +1,26 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { useT } from "@/lib/i18n";
+
+export function AccountClient({ user }: { user: any }) {
+ const t = useT();
+ return
+
{t("account.title")}
+
+
+ {t("account.account")}
+ {user.email}
+ {t("account.role")}: {user.role}
+
+
+ {t("account.plan")}
+ {user.subscription?.plan.name || "—"}
+ {user.subscription?.status || t("account.noSubscription")}
+
+
+ {t("account.creditBalance")}
+ {user.creditBalance.toLocaleString("tr-TR")}
+
+
+
;
+}
\ No newline at end of file
diff --git a/src/app/dashboard/account/page.tsx b/src/app/dashboard/account/page.tsx
index e0d1fd7..c3a8cf2 100644
--- a/src/app/dashboard/account/page.tsx
+++ b/src/app/dashboard/account/page.tsx
@@ -1,7 +1,7 @@
import { requireUser } from "@/lib/auth/current-user";
-import { Card } from "@/components/ui/card";
+import { AccountClient } from "./account-client";
export default async function AccountPage() {
const user = await requireUser();
- return Hesabım
Hesap
{user.email}
Yetki: {user.role}
Plan
{user.subscription?.plan.name || "—"}
{user.subscription?.status || "Abonelik yok"}
Kredi bakiyesi
{user.creditBalance.toLocaleString("tr-TR")}
;
+ return ;
}
diff --git a/src/app/dashboard/admin/admin-client.tsx b/src/app/dashboard/admin/admin-client.tsx
new file mode 100644
index 0000000..3c0b367
--- /dev/null
+++ b/src/app/dashboard/admin/admin-client.tsx
@@ -0,0 +1,78 @@
+"use client";
+import Link from "next/link";
+import { AdminNav } from "@/components/admin/admin-nav";
+import { Card } from "@/components/ui/card";
+import { useT } from "@/lib/i18n";
+
+export function AdminClient({ users, admins, activeSubscriptions, paid, totalCredits, recentPayments, recentAudits, money }: {
+ users: number;
+ admins: number;
+ activeSubscriptions: number;
+ paid: { _sum: { amountCents: number | null }; _count: number };
+ totalCredits: { _sum: { creditBalance: number | null } };
+ recentPayments: any[];
+ recentAudits: any[];
+ money: (cents: number | null | undefined, currency?: string) => string;
+}) {
+ const t = useT();
+ return (
+
+
+
+
{t("admin.controlCenter")}
+
{t("admin.title")}
+
{t("admin.description")}
+
+
{t("admin.systemActive")}
+
+
+
+
{t("admin.totalUsers")}
{users}
{admins} {t("admin.admins")}
+
{t("admin.activeSubscriptions")}
{activeSubscriptions}
+
{t("admin.revenue30d")}
{money(paid._sum.amountCents)}
{paid._count} {t("admin.payments")}
+
{t("admin.distributedCredits")}
{(totalCredits._sum.creditBalance || 0).toLocaleString("tr-TR")}
+
{t("admin.quickAction")}
{t("admin.manageUsers")}
+
+
+
+
+
+
{t("admin.recentPayments")}
+ {t("admin.viewAll")}
+
+
+ {recentPayments.map((payment) => (
+
+
+
{payment.user.email}
+
{payment.provider} · {payment.status}
+
+
{money(payment.amountCents, payment.currency)}
+
+ ))}
+ {!recentPayments.length &&
{t("admin.noPaymentsYet")}
}
+
+
+
+
+
{t("admin.recentAdminActions")}
+ {t("admin.logCenter")}
+
+
+ {recentAudits.map((log) => (
+
+
+ {log.action}
+ {log.createdAt.toLocaleString("tr-TR")}
+
+
{log.summary}
+
{log.actorEmail}
+
+ ))}
+ {!recentAudits.length &&
{t("admin.noAuditLogsYet")}
}
+
+
+
+
+ );
+}
\ No newline at end of file
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")}
| {t("adminLogs.user")} | {t("adminLogs.type")} | {t("adminLogs.amount")} | {t("adminLogs.balance")} | {t("adminLogs.reason")} | {t("adminLogs.date")} |
{credits.map((tx) => | {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
| Kullanıcı | Tür | Miktar | Bakiye | Neden | Tarih |
{credits.map((tx) => | {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/page.tsx b/src/app/dashboard/admin/page.tsx
index bf5ca8d..7604ac6 100644
--- a/src/app/dashboard/admin/page.tsx
+++ b/src/app/dashboard/admin/page.tsx
@@ -1,7 +1,7 @@
import Link from "next/link";
import { PaymentStatus, SubscriptionStatus } from "@prisma/client";
import { AdminNav } from "@/components/admin/admin-nav";
-import { Card } from "@/components/ui/card";
+import { AdminClient } from "./admin-client";
import { requireAdmin } from "@/lib/auth/current-user";
import { prisma } from "@/lib/db";
@@ -22,31 +22,5 @@ export default async function AdminOverviewPage() {
prisma.adminAuditLog.findMany({ orderBy: { createdAt: "desc" }, take: 8 })
]);
- return (
-
-
-
Kontrol Merkezi
AdSeeQ Admin
Kullanıcı, gelir, kredi ve operasyon görünümü.
-
Sistem aktif
-
-
-
-
Toplam kullanıcı
{users}
{admins} admin
-
Aktif abonelik
{activeSubscriptions}
-
30 gün gelir
{money(paid._sum.amountCents)}
{paid._count} ödeme
-
Dağıtılan kredi
{(totalCredits._sum.creditBalance || 0).toLocaleString("tr-TR")}
-
Hızlı işlem
Kullanıcı yönet →
-
-
-
-
- Son ödemeler
Tümü
- {recentPayments.map((payment) =>
{payment.user.email}
{payment.provider} · {payment.status}
{money(payment.amountCents, payment.currency)}
)}{!recentPayments.length &&
Henüz ödeme kaydı yok.
}
-
-
- Son admin işlemleri
Log merkezi
- {recentAudits.map((log) =>
{log.action}{log.createdAt.toLocaleString("tr-TR")}
{log.summary}
{log.actorEmail}
)}{!recentAudits.length &&
Henüz audit kaydı yok.
}
-
-
-
- );
+ return ;
}
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.
-
- | Kullanıcı | Tutar | Sağlayıcı | Kredi | Referans | Tarih | Durum |
{payments.map((payment) => | {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")}
+
+ | {t("adminPayments.user")} | {t("adminPayments.amount")} | {t("adminPayments.provider")} | {t("adminPayments.credit")} | {t("adminPayments.reference")} | {t("adminPayments.date")} | {t("adminPayments.status")} |
{payments.map((payment) => | {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/ads/ads-client.tsx b/src/app/dashboard/ads/ads-client.tsx
new file mode 100644
index 0000000..7032330
--- /dev/null
+++ b/src/app/dashboard/ads/ads-client.tsx
@@ -0,0 +1,95 @@
+"use client";
+import Link from "next/link";
+import { Card } from "@/components/ui/card";
+import { AdCreativeMedia } from "@/components/ad-creative-media";
+import { ApifyEmptySearch } from "@/components/ads/apify-empty-search";
+import { AD_COUNTRIES } from "@/lib/countries";
+import { useT } from "@/lib/i18n";
+
+export function AdsClient({ plan, q, niche, mediaType, country, status, matchMode, minDays, language, sort, ads, apifyPlanLimit }: {
+ plan: any;
+ q: string | undefined;
+ niche: string | undefined;
+ mediaType: string | undefined;
+ country: string | undefined;
+ status: string;
+ matchMode: string;
+ minDays: number;
+ language: string | undefined;
+ sort: string;
+ ads: any[];
+ apifyPlanLimit: number;
+}) {
+ const t = useT();
+ return (
+
+
+
+
Search Meta Adlibrary
+
{t("ads.description")}
+
+
{t("ads.planLabel")}: {plan?.name} · {t("ads.planWarning")}
+
+
+
+ {["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => {x})}
+
+
+ {q &&
}
+ {ads.map((ad: any) => (
+
+ {ad.isLocked && {t("ads.unlockWinners")}
}
+
+
+
+
{ad.headline}
+
Top %{Math.round(ad.rankPercentile || 0)}
+
+
{ad.brandPage?.name} · {ad.mediaType} · {ad.daysRunning} {t("ads.days")}
+
{ad.primaryText}
+
+
{ad.countries.join(", ")}
{t("ads.country")}
+
€{ad.estimatedSpendMin ?? "—"}
{t("ads.minSpend")}
+
{ad.adScore}
{t("ads.score")}
+
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/dashboard/ads/page.tsx b/src/app/dashboard/ads/page.tsx
index fcd5fac..71be726 100644
--- a/src/app/dashboard/ads/page.tsx
+++ b/src/app/dashboard/ads/page.tsx
@@ -9,6 +9,7 @@ import { AdCreativeMedia } from "@/components/ad-creative-media";
import { ApifyEmptySearch } from "@/components/ads/apify-empty-search";
import { AD_COUNTRIES } from "@/lib/countries";
import { AdSearchMatchMode, adSearchDatabaseTerms, adSearchRelevance } from "@/lib/ad-search";
+import { AdsClient } from "./ads-client";
export default async function AdsPage({ searchParams }: { searchParams: Promise> }) {
const user = await requireUser();
@@ -63,75 +64,5 @@ export default async function AdsPage({ searchParams }: { searchParams: Promise<
: ads;
const masked = relevantAds.map((ad) => maskAdForPlan({ ...ad, isSaved: ad.savedBy.length > 0 }, plan?.code, isAdmin));
- return (
-
-
-
-
Search Meta Adlibrary
-
Kazanan Meta reklamlarını anahtar kelime, ülke, niş ve medya tipine göre keşfet.
-
-
Plan: {plan?.name} · Free ise kartlar kilitli
-
-
-
- {["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => {x})}
-
-
- {q &&
}
- {masked.map((ad: any) => (
-
- {ad.isLocked && Start now — Unlock winners
}
-
-
-
-
{ad.headline}
-
Top %{Math.round(ad.rankPercentile || 0)}
-
-
{ad.brandPage?.name} · {ad.mediaType} · {ad.daysRunning} gün
-
{ad.primaryText}
-
-
{ad.countries.join(", ")}
Ülke
-
€{ad.estimatedSpendMin ?? "—"}
Min spend
-
{ad.adScore}
Score
-
-
-
- ))}
-
-
- );
+ return ;
}
diff --git a/src/app/dashboard/brand-tracker/brand-tracker-client.tsx b/src/app/dashboard/brand-tracker/brand-tracker-client.tsx
new file mode 100644
index 0000000..3dc98f9
--- /dev/null
+++ b/src/app/dashboard/brand-tracker/brand-tracker-client.tsx
@@ -0,0 +1,52 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { BrandTrackerActions } from "@/components/brand-tracker-actions";
+import { useT } from "@/lib/i18n";
+
+export function BrandTrackerClient({ query, brands, tracked, alerts, trackedByBrand, limit }: {
+ query: string | undefined;
+ brands: any[];
+ tracked: any[];
+ alerts: any[];
+ trackedByBrand: Map;
+ limit: number | null;
+}) {
+ const t = useT();
+ return (
+
+
+
{t("brand.title")}
{t("brand.description")}
+
{tracked.length} {t("brand.trackingLabel")} · {t("brand.limitLabel")}: {limit === null ? t("brand.unlimited") : limit}
+
+
+ {alerts.length > 0 &&
{t("brand.newAlerts")}
{alerts.map((alert) =>
{alert.trackedBrand.brandPage.name}{alert.title}
{alert.createdAt.toLocaleDateString("tr-TR")})}
}
+
+ {brands.map((brand) => {
+ const activeAds = brand.ads.filter((ad: any) => ad.status === "ACTIVE").length;
+ return
+
+
+
{brand.name.slice(0, 2).toUpperCase()}
+
{brand.name}
{brand.websiteDomain || brand.pageUrl || t("brand.metaPage")}
+
+
+
+
+
{brand._count.ads}
{t("brand.totalAds")}
+
{activeAds}
{t("brand.recentActive")}
+
{brand.niche || "—"}
{t("brand.niche")}
+
+
+ {brand.ads.map((ad: any) =>
{ad.headline || t("brand.headlessCreative")}{ad.mediaType} · {ad.daysRunning || "—"} {t("brand.days")}
)}
+ {!brand.ads.length &&
{t("brand.noAdsYet")}
}
+
+ ;
+ })}
+ {!brands.length &&
{t("brand.noResults")}}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/dashboard/brand-tracker/page.tsx b/src/app/dashboard/brand-tracker/page.tsx
index 10a5544..cbfa294 100644
--- a/src/app/dashboard/brand-tracker/page.tsx
+++ b/src/app/dashboard/brand-tracker/page.tsx
@@ -2,8 +2,7 @@ import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/db";
import { requireUser } from "@/lib/auth/current-user";
import { planFromUser, getFeatureLimit } from "@/lib/plans";
-import { Card } from "@/components/ui/card";
-import { BrandTrackerActions } from "@/components/brand-tracker-actions";
+import { BrandTrackerClient } from "./brand-tracker-client";
export default async function BrandTrackerPage({ searchParams }: { searchParams: Promise> }) {
const user = await requireUser();
@@ -31,34 +30,5 @@ export default async function BrandTrackerPage({ searchParams }: { searchParams:
const trackedByBrand = new Map(tracked.map((item) => [item.brandPageId, item.id]));
const limit = getFeatureLimit(planFromUser(user as any), "followed_brands");
- return (
-
-
-
Brand Tracker
Markaları takip et; aktif reklam sayısını ve son kreatiflerini tek ekranda izle.
-
{tracked.length} takip · Limit: {limit === null ? "Sınırsız" : limit}
-
-
- {alerts.length > 0 &&
Yeni reklam uyarıları
{alerts.map((alert) =>
{alert.trackedBrand.brandPage.name}{alert.title}
{alert.createdAt.toLocaleDateString("tr-TR")})}
}
-
- {brands.map((brand) => {
- const activeAds = brand.ads.filter((ad) => ad.status === "ACTIVE").length;
- return
-
-
-
{brand.name.slice(0, 2).toUpperCase()}
-
{brand.name}
{brand.websiteDomain || brand.pageUrl || "Meta marka sayfası"}
-
-
-
- {brand._count.ads}
Toplam reklam
{activeAds}
Son 3 aktif
{brand.niche || "—"}
Niche
- {brand.ads.map((ad) =>
{ad.headline || "Başlıksız kreatif"}{ad.mediaType} · {ad.daysRunning || "—"} gün
)}{!brand.ads.length &&
Henüz reklam verisi yok.
}
- ;
- })}
- {!brands.length &&
Aramanızla eşleşen marka bulunamadı.}
-
-
- );
+ return ;
}
diff --git a/src/app/dashboard/dashboard-layout-client.tsx b/src/app/dashboard/dashboard-layout-client.tsx
new file mode 100644
index 0000000..fd5b5dd
--- /dev/null
+++ b/src/app/dashboard/dashboard-layout-client.tsx
@@ -0,0 +1,39 @@
+"use client";
+import Link from "next/link";
+import { DashboardMobileNav } from "@/components/dashboard-mobile-nav";
+import { BrandLogo } from "@/components/brand-logo";
+import { useT } from "@/lib/i18n";
+
+export function DashboardLayoutClient({ user, nav, children }: {
+ user: any;
+ nav: [string, string][];
+ children: React.ReactNode;
+}) {
+ const t = useT();
+ return (
+ <>
+
+
+
+
+
+
+ {user.role !== "ADMIN" &&
{t("nav.upgrade")}}
+
+
{user.name || user.email}
+
{user.subscription?.plan.name}
+
+
+
+
+ {children}
+ >
+ );
+}
\ No newline at end of file
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
index 396802e..4b5489c 100644
--- a/src/app/dashboard/layout.tsx
+++ b/src/app/dashboard/layout.tsx
@@ -1,6 +1,6 @@
import Link from "next/link";
import { requireUser } from "@/lib/auth/current-user";
-import { DashboardMobileNav } from "@/components/dashboard-mobile-nav";
+import { DashboardLayoutClient } from "./dashboard-layout-client";
import { BrandLogo } from "@/components/brand-logo";
import type { Metadata } from "next";
@@ -9,7 +9,7 @@ export const metadata: Metadata = {
robots: { index: false, follow: false, nocache: true }
};
-const nav = [
+const nav: [string, string][] = [
["Ads", "/dashboard/ads"],
["Stores", "/dashboard/stores"],
["Store Tracker", "/dashboard/store-tracker"],
@@ -25,28 +25,9 @@ export default async function DashboardLayout({ children }: { children: React.Re
const user = await requireUser();
return (
-
-
-
-
-
-
- {user.role !== "ADMIN" &&
Upgrade}
-
-
{user.name || user.email}
-
{user.subscription?.plan.name}
-
-
-
-
-
{children}
+
+ {children}
+
);
}
diff --git a/src/app/dashboard/magic-ai/magic-ai-client.tsx b/src/app/dashboard/magic-ai/magic-ai-client.tsx
new file mode 100644
index 0000000..66bdf01
--- /dev/null
+++ b/src/app/dashboard/magic-ai/magic-ai-client.tsx
@@ -0,0 +1,35 @@
+"use client";
+import { Card } from "@/components/ui/card";
+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 }: {
+ q: string | undefined;
+ ads: any[];
+ analyses: any[];
+}) {
+ 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")}
+
+ {analyses.length > 0 &&
{analyses.map((analysis) =>
{analysis.query || t("ai.generalAnalysis")}
{analysis.model}{analysis.summary}
{(analysis.insights as Array<{title:string;action:string;confidence:number}>).map((item, index) =>
{item.title}{item.action}
)}
)}
}
+
Creative Intelligence
{ads.length} {t("ai.actionPlanTitle")}
{t("ai.actionPlanDescription")}
+
{ads.map((ad) => { const insight = recommendation(ad); return
+ {ad.headline || t("ai.headlessAd")}
{ad.brandPage?.name || t("ai.unknownBrand")} · {ad.mediaType} · {ad.daysRunning || "—"} {t("ai.days")}
{insight.label}{t("ai.hook")}
{sentence(ad.primaryText)}
{insight.text}
+ ; })}{!ads.length &&
{t("ai.noAdsToAnalyze")}}
+
+ );
+}
diff --git a/src/app/dashboard/magic-ai/page.tsx b/src/app/dashboard/magic-ai/page.tsx
index b15ad3f..e79d58c 100644
--- a/src/app/dashboard/magic-ai/page.tsx
+++ b/src/app/dashboard/magic-ai/page.tsx
@@ -1,21 +1,7 @@
import { Prisma } from "@prisma/client";
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 { MagicAiRunner } from "@/components/magic-ai-runner";
-
-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." };
-}
+import { MagicAiClient } from "./magic-ai-client";
export default async function MagicAIPage({ searchParams }: { searchParams: Promise> }) {
const user = await requireUser();
@@ -26,15 +12,5 @@ export default async function MagicAIPage({ searchParams }: { searchParams: Prom
prisma.aiAnalysis.findMany({ where: { userId: user.id }, orderBy: { createdAt: "desc" }, take: 5 })
]);
- return (
-
-
Magic AI
Kreatif dayanıklılığı, formatı ve reklam metnini analiz ederek uygulanabilir test önerileri üretir.
-
- {analyses.length > 0 &&
{analyses.map((analysis) =>
{analysis.query || "Genel kreatif analizi"}
{analysis.model}{analysis.summary}
{(analysis.insights as Array<{title:string;action:string;confidence:number}>).map((item, index) =>
{item.title}{item.action}
)}
)}
}
-
Creative Intelligence
{ads.length} reklamdan aksiyon planı
Öneriler canlı reklam süresi ve mevcut kreatif sinyallerinden hesaplanır.
-
{ads.map((ad) => { const insight = recommendation(ad); return
- {ad.headline || "Başlıksız reklam"}
{ad.brandPage?.name || "Bilinmeyen marka"} · {ad.mediaType} · {ad.daysRunning || "—"} gün
{insight.label}Kanca
{sentence(ad.primaryText)}
{insight.text}
- ; })}{!ads.length &&
Analiz edilecek reklam bulunamadı.}
-
- );
+ return ;
}
diff --git a/src/app/dashboard/saved-ads/page.tsx b/src/app/dashboard/saved-ads/page.tsx
index 9015e96..a8cd9bd 100644
--- a/src/app/dashboard/saved-ads/page.tsx
+++ b/src/app/dashboard/saved-ads/page.tsx
@@ -1,24 +1,10 @@
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 { SavedAdsClient } from "./saved-ads-client";
export default async function SavedAdsPage() {
const user = await requireUser();
const folders = await prisma.savedFolder.findMany({ where: { userId: user.id } });
const saved = await prisma.savedAd.findMany({ where: { userId: user.id }, include: { folder: true, ad: { include: { brandPage: true, creatives: { take: 1 } } } }, orderBy: { createdAt: "desc" } });
- return (
-
-
Saved Ads
-
-
- Folders
- All Saved Ads ({saved.length})
{folders.map((f) =>
{f.name}
)}
-
-
- {saved.map((s) =>
{s.ad.headline}
{s.ad.brandPage?.name} · {s.folder?.name || "All"}
{s.ad.primaryText}
)}
-
-
-
- );
+ return ;
}
diff --git a/src/app/dashboard/saved-ads/saved-ads-client.tsx b/src/app/dashboard/saved-ads/saved-ads-client.tsx
new file mode 100644
index 0000000..27dd59a
--- /dev/null
+++ b/src/app/dashboard/saved-ads/saved-ads-client.tsx
@@ -0,0 +1,32 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { AdCreativeMedia } from "@/components/ad-creative-media";
+import { useT } from "@/lib/i18n";
+
+export function SavedAdsClient({ folders, saved }: { folders: any[]; saved: any[] }) {
+ const t = useT();
+ return (
+
+
{t("savedAds.title")}
+
+
+ {t("savedAds.folders")}
+
+
{t("savedAds.allAds")} ({saved.length})
+ {folders.map((f) =>
{f.name}
)}
+
+
+
+ {saved.map((s) => (
+
+
+ {s.ad.headline}
+ {s.ad.brandPage?.name} · {s.folder?.name || t("savedAds.all")}
+ {s.ad.primaryText}
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/dashboard/store-tracker/page.tsx b/src/app/dashboard/store-tracker/page.tsx
index 2f98d07..365dcd9 100644
--- a/src/app/dashboard/store-tracker/page.tsx
+++ b/src/app/dashboard/store-tracker/page.tsx
@@ -3,18 +3,11 @@ import { requireUser } from "@/lib/auth/current-user";
import { Card } from "@/components/ui/card";
import { StoreTrackerManager } from "@/components/store-tracker-manager";
import { getFeatureLimit, planFromUser } from "@/lib/plans";
+import { StoreTrackerClient } from "./store-tracker-client";
export default async function StoreTrackerPage() {
const user = await requireUser();
const tracked = await prisma.trackedStore.findMany({ where: { userId: user.id }, include: { store: { include: { products: { where: { isBestSeller: true }, take: 2 } } } }, orderBy: { createdAt: "desc" } });
const limit = getFeatureLimit(planFromUser(user as any), "tracked_stores");
- return (
-
-
Store Tracker
-
Plan limitine göre rakip Shopify mağazalarını watchlist’e ekle.
-
-
-
-
- );
+ return ;
}
diff --git a/src/app/dashboard/store-tracker/store-tracker-client.tsx b/src/app/dashboard/store-tracker/store-tracker-client.tsx
new file mode 100644
index 0000000..530a98d
--- /dev/null
+++ b/src/app/dashboard/store-tracker/store-tracker-client.tsx
@@ -0,0 +1,20 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { StoreTrackerManager } from "@/components/store-tracker-manager";
+import { useT } from "@/lib/i18n";
+
+export function StoreTrackerClient({ tracked, limit }: {
+ tracked: any[];
+ limit: number | null;
+}) {
+ const t = useT();
+ return (
+
+
{t("store.title")}
+
{t("store.description")}
+
+
+
+
+ );
+}
\ No newline at end of file
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 (
+
+
+
+
{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/app/dashboard/stores/page.tsx b/src/app/dashboard/stores/page.tsx
index 6ad3ab2..1d1f363 100644
--- a/src/app/dashboard/stores/page.tsx
+++ b/src/app/dashboard/stores/page.tsx
@@ -2,7 +2,7 @@ import Link from "next/link";
import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/db";
import { requireUser } from "@/lib/auth/current-user";
-import { Card } from "@/components/ui/card";
+import { StoresClient } from "./stores-client";
export default async function StoresPage({ searchParams }: { searchParams: Promise> }) {
await requireUser();
@@ -13,39 +13,5 @@ export default async function StoresPage({ searchParams }: { searchParams: Promi
if (q) where.OR = [{ name: { contains: q, mode: "insensitive" } }, { domain: { contains: q, mode: "insensitive" } }, { niche: { contains: q, mode: "insensitive" } }];
if (niche) where.niche = { contains: niche, mode: "insensitive" };
const stores = await prisma.store.findMany({ where, include: { products: { where: { isBestSeller: true }, take: 2 }, brandPages: { include: { _count: { select: { ads: true } } }, take: 1 } }, orderBy: { estRevenue30dMax: "desc" }, take: 50 });
- return (
-
-
-
Explore Stores
-
Shopify mağazalarını trafik, gelir, niche ve aktif reklam sayısıyla keşfet.
-
-
-
-
-
-
- | Shop Info | Best Sellers | Niche | Monthly Visits | Est. Revenue 30d | Meta Ads | |
-
-
- {stores.map((s) => (
-
-  {s.name} {s.domain} · {s.country} |
- {s.products.map((p) => p.title).join(", ")} |
- {s.niche} |
- {s.monthlyVisits?.toLocaleString()} +{s.monthlyVisitGrowth}% |
- €{s.estRevenue30dMin?.toLocaleString()}–€{s.estRevenue30dMax?.toLocaleString()} |
- {s.brandPages[0]?._count.ads || 0} |
- Details |
-
- ))}
-
-
-
-
-
- );
+ return ;
}
diff --git a/src/app/dashboard/stores/stores-client.tsx b/src/app/dashboard/stores/stores-client.tsx
new file mode 100644
index 0000000..6cbec6c
--- /dev/null
+++ b/src/app/dashboard/stores/stores-client.tsx
@@ -0,0 +1,69 @@
+"use client";
+import Link from "next/link";
+import { Card } from "@/components/ui/card";
+import { useT } from "@/lib/i18n";
+
+export function StoresClient({ q, niche, stores }: {
+ q: string | undefined;
+ niche: string | undefined;
+ stores: any[];
+}) {
+ const t = useT();
+ return (
+
+
+
{t("stores.title")}
+
{t("stores.description")}
+
+
+
+
+
+
+
+ | {t("stores.shopInfo")} |
+ {t("stores.bestSellers")} |
+ {t("stores.niche")} |
+ {t("stores.monthlyVisits")} |
+ {t("stores.estRevenue")} |
+ {t("stores.metaAds")} |
+ |
+
+
+
+ {stores.map((s) => (
+
+
+
+ 
+
+ {s.name}
+ {s.domain} · {s.country}
+
+
+ |
+ {s.products.map((p: any) => p.title).join(", ")} |
+ {s.niche} |
+ {s.monthlyVisits?.toLocaleString()} +{s.monthlyVisitGrowth}% |
+ €{s.estRevenue30dMin?.toLocaleString()}–€{s.estRevenue30dMax?.toLocaleString()} |
+ {s.brandPages[0]?._count.ads || 0} |
+ {t("stores.details")} |
+
+ ))}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/dashboard/tiktok-shop/page.tsx b/src/app/dashboard/tiktok-shop/page.tsx
index ac5711a..bee1a44 100644
--- a/src/app/dashboard/tiktok-shop/page.tsx
+++ b/src/app/dashboard/tiktok-shop/page.tsx
@@ -2,8 +2,7 @@ import { Prisma } from "@prisma/client";
import { prisma } from "@/lib/db";
import { requireUser } from "@/lib/auth/current-user";
import { planFromUser } from "@/lib/plans";
-import { Card } from "@/components/ui/card";
-import { TikTokShopImport } from "@/components/tiktok-shop-import";
+import { TikTokShopClient } from "./tiktok-shop-client";
function opportunityScore(product: { soldCount: number | null; rating: number | null; reviewCount: number | null; price: number | null }) {
const sales = Math.min(45, Math.log10(Math.max(1, product.soldCount || 1)) * 10);
@@ -27,20 +26,5 @@ export default async function TikTokShopPage({ searchParams }: { searchParams: P
const products = await prisma.tikTokProduct.findMany({ where, orderBy: [{ soldCount: "desc" }, { lastSeenAt: "desc" }], take: 60 });
const ranked = products.map((product) => ({ ...product, score: opportunityScore(product) })).sort((a, b) => b.score - a.score);
const regions = [...new Set(products.map((product) => product.region).filter(Boolean))] as string[];
- return
-
TikTok Shop
Canlı ürün, satış, fiyat, mağaza ve değerlendirme sinyalleri.
-
-
-
Canlı ürün
{ranked.length}
Mağaza
{new Set(ranked.map((item) => item.shopExternalId || item.shopName)).size}
Toplam satış sinyali
{ranked.reduce((sum, item) => sum + (item.soldCount || 0), 0).toLocaleString("tr-TR")}
-
{ranked.map((product) =>
- {product.imageUrl ?
: Ürün görseli yok
}
- {product.title}
{product.shopName || "Mağaza bilinmiyor"} · {product.region}
{product.score}
- {product.price ? `${product.price} ${product.currency || ""}` : "—"}
Fiyat
{(product.soldCount || 0).toLocaleString("tr-TR")}
Satış
{product.rating || "—"}
Puan
- {product.productUrl && TikTok’ta aç →}
- )}{!ranked.length &&
Yukarıdan bir ürün aratıp canlı veriyi getirin.}
-
;
+ return ;
}
diff --git a/src/app/dashboard/tiktok-shop/tiktok-shop-client.tsx b/src/app/dashboard/tiktok-shop/tiktok-shop-client.tsx
new file mode 100644
index 0000000..e9bc789
--- /dev/null
+++ b/src/app/dashboard/tiktok-shop/tiktok-shop-client.tsx
@@ -0,0 +1,38 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { TikTokShopImport } from "@/components/tiktok-shop-import";
+import { useT } from "@/lib/i18n";
+
+export function TikTokShopClient({ planLimit, q, region, ranked, regions }: {
+ planLimit: number;
+ q: string | undefined;
+ region: string | undefined;
+ ranked: any[];
+ regions: string[];
+}) {
+ const t = useT();
+ return
+
{t("tiktok.title")}
{t("tiktok.description")}
+
+
+
+
{t("tiktok.liveProducts")}
{ranked.length}
+
{t("tiktok.stores")}
{new Set(ranked.map((item) => item.shopExternalId || item.shopName)).size}
+
{t("tiktok.totalSalesSignal")}
{ranked.reduce((sum, item) => sum + (item.soldCount || 0), 0).toLocaleString("tr-TR")}
+
+
{ranked.map((product) =>
+ {product.imageUrl ?
: {t("tiktok.noProductImage")}
}
+ {product.title}
{product.shopName || t("tiktok.storeUnknown")} · {product.region}
{product.score}
+
+
{product.price ? `${product.price} ${product.currency || ""}` : "—"}
{t("tiktok.price")}
+
{(product.soldCount || 0).toLocaleString("tr-TR")}
{t("tiktok.sales")}
+
{product.rating || "—"}
{t("tiktok.score")}
+
+ {product.productUrl && {t("tiktok.openOnTiktok")}}
+ )}{!ranked.length &&
{t("tiktok.searchFirst")}}
+
;
+}
\ No newline at end of file
diff --git a/src/app/dashboard/trends/page.tsx b/src/app/dashboard/trends/page.tsx
index 6784fcd..b974561 100644
--- a/src/app/dashboard/trends/page.tsx
+++ b/src/app/dashboard/trends/page.tsx
@@ -1,6 +1,6 @@
import { prisma } from "@/lib/db";
import { requireUser } from "@/lib/auth/current-user";
-import { Card } from "@/components/ui/card";
+import { TrendsClient } from "./trends-client";
function countBy(values: (string | null | undefined)[]) {
const counts = new Map();
@@ -8,10 +8,6 @@ function countBy(values: (string | null | undefined)[]) {
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
}
-function Bars({ rows, total }: { rows: [string, number][]; total: number }) {
- return {rows.slice(0, 8).map(([label, value]) =>
)}
;
-}
-
export default async function TrendsPage() {
await requireUser();
const [ads, stores, history] = await Promise.all([
@@ -26,13 +22,5 @@ export default async function TrendsPage() {
const media = countBy(ads.map((ad) => ad.mediaType));
const countries = countBy(ads.flatMap((ad) => ad.countries));
- return (
-
-
Trends
Reklam formatı, niche, ülke ve mağaza büyüme sinyallerini canlı veriden karşılaştır.
-
İzlenen reklam
{ads.length}
Aktif reklam
{active.length}
30+ gün yaşayan
{longRunners.length}
Son 14 gün
{newAds.length}
-
Niche dağılımı
Medya formatı
Ülke sinyali
sum + ad.countries.length, 0)} />
-
Hızlı büyüyen mağazalar
Aylık ziyaret büyümesine göre
{stores.slice(0, 8).map((store) =>
{store.name}
{store.niche || "Niche yok"} · {store.country || "—"}
+{store.monthlyVisitGrowth || 0}%
)}
-
14 günlük geçmiş
Günlük otomasyonla kaydedilen reklam ve TikTok Shop sinyalleri
{history.map((snapshot) => { const metrics = snapshot.metrics as any; return
{snapshot.date.toLocaleDateString("tr-TR")}{metrics.ads?.active || 0} aktif reklam
{metrics.tiktok?.total || 0} TikTok ürün
; })}{!history.length &&
İlk günlük snapshot henüz oluşmadı.
}
-
- );
+ return ;
}
diff --git a/src/app/dashboard/trends/trends-client.tsx b/src/app/dashboard/trends/trends-client.tsx
new file mode 100644
index 0000000..847f157
--- /dev/null
+++ b/src/app/dashboard/trends/trends-client.tsx
@@ -0,0 +1,68 @@
+"use client";
+import { Card } from "@/components/ui/card";
+import { useT } from "@/lib/i18n";
+
+function Bars({ rows, total }: { rows: [string, number][]; total: number }) {
+ return {rows.slice(0, 8).map(([label, value]) =>
)}
;
+}
+
+export function TrendsClient({ ads, active, longRunners, newAds, niches, media, countries, stores, history }: {
+ ads: any[];
+ active: any[];
+ longRunners: any[];
+ newAds: any[];
+ niches: [string, number][];
+ media: [string, number][];
+ countries: [string, number][];
+ stores: any[];
+ history: any[];
+}) {
+ const t = useT();
+ return (
+
+
{t("trends.title")}
{t("trends.description")}
+
+
{t("trends.totalAds")}
{ads.length}
+
{t("trends.activeAds")}
{active.length}
+
{t("trends.longRunners")}
{longRunners.length}
+
{t("trends.newAds")}
{newAds.length}
+
+
+ {t("trends.nicheDistribution")}
+ {t("trends.mediaFormat")}
+ {t("trends.countrySignal")}
sum + ad.countries.length, 0)} />
+
+
+
+
{t("trends.fastGrowingStores")}
{t("trends.byMonthlyGrowth")}
+
+
+ {stores.slice(0, 8).map((store) => (
+
+
{store.name}
+
{store.niche || t("trends.noNiche")} · {store.country || "—"}
+
+{store.monthlyVisitGrowth || 0}%
+
+ ))}
+
+
+
+ {t("trends.historyTitle")}
+ {t("trends.historyDescription")}
+
+ {history.map((snapshot) => {
+ const metrics = snapshot.metrics as any;
+ return (
+
+
{snapshot.date.toLocaleDateString("tr-TR")}
+
{metrics.ads?.active || 0} {t("trends.activeAds")}
+
{metrics.tiktok?.total || 0} {t("trends.tiktokProducts")}
+
+ );
+ })}
+ {!history.length &&
{t("trends.noSnapshotYet")}
}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index fe04e85..7335916 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { SITE_DESCRIPTION, SITE_NAME, SITE_URL } from "@/lib/seo";
+import { LangSwitch } from "@/components/LangSwitch";
export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
@@ -44,7 +45,12 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
- {children}
+
+
+
+
+ {children}
+
);
}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
index b4e4634..88cf369 100644
--- a/src/app/login/page.tsx
+++ b/src/app/login/page.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import { BrandLogo } from "@/components/brand-logo";
+import { useT } from "@/lib/i18n";
function GoogleIcon() {
return (
@@ -15,6 +16,7 @@ function GoogleIcon() {
}
export default function LoginPage() {
+ const t = useT();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
@@ -22,13 +24,13 @@ export default function LoginPage() {
async function submit(e: React.FormEvent) {
e.preventDefault();
setError("");
- const res = await fetch("/api/auth/login", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ email, password })
+ const res = await fetch("/api/auth/login", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ email, password })
});
if (!res.ok) {
- setError("E-posta veya şifre hatalı.");
+ setError(t("auth.loginError"));
return;
}
location.href = "/dashboard/ads";
@@ -38,50 +40,50 @@ export default function LoginPage() {
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 433a074..f8498e6 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,8 +1,13 @@
+"use client";
+
import { LinkButton } from "@/components/ui/button";
import { BrandLogo } from "@/components/brand-logo";
import { homeFaq, homeStructuredData, serializeJsonLd } from "@/lib/seo";
+import { useT } from "@/lib/i18n";
export default function LandingPage() {
+ const t = useT();
+
return (