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/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/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..5995d49
--- /dev/null
+++ b/src/app/dashboard/magic-ai/magic-ai-client.tsx
@@ -0,0 +1,26 @@
+"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, sentence, recommendation }: {
+ q: string | undefined;
+ ads: any[];
+ analyses: any[];
+ sentence: (value?: string | null) => string;
+ recommendation: (ad: any) => { label: string; color: string; text: string };
+}) {
+ const t = useT();
+ 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")}}
+
+ );
+}
\ 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 b15ad3f..181bfa9 100644
--- a/src/app/dashboard/magic-ai/page.tsx
+++ b/src/app/dashboard/magic-ai/page.tsx
@@ -1,9 +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";
+import { MagicAiClient } from "./magic-ai-client";
function sentence(value?: string | null) {
return value?.split(/[.!?\n]/).map((item) => item.trim()).find(Boolean) || "Metin kancası bulunamadı";
@@ -26,15 +24,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/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 (