From f7861d6fd167c23b2ea9b3b3215389bdf397d78a Mon Sep 17 00:00:00 2001 From: ht Date: Tue, 25 Aug 2026 20:11:00 +0300 Subject: [PATCH] feat: wire remaining dashboard pages to i18n (pass 2+3) --- package-lock.json | 1 - src/app/dashboard/account/account-client.tsx | 26 ++ src/app/dashboard/account/page.tsx | 4 +- src/app/dashboard/admin/admin-client.tsx | 78 ++++ src/app/dashboard/admin/page.tsx | 30 +- src/app/dashboard/ads/ads-client.tsx | 95 +++++ src/app/dashboard/ads/page.tsx | 73 +--- .../brand-tracker/brand-tracker-client.tsx | 52 +++ src/app/dashboard/brand-tracker/page.tsx | 34 +- src/app/dashboard/dashboard-layout-client.tsx | 39 ++ src/app/dashboard/layout.tsx | 29 +- .../dashboard/magic-ai/magic-ai-client.tsx | 26 ++ src/app/dashboard/magic-ai/page.tsx | 16 +- src/app/dashboard/saved-ads/page.tsx | 18 +- .../dashboard/saved-ads/saved-ads-client.tsx | 32 ++ src/app/dashboard/store-tracker/page.tsx | 11 +- .../store-tracker/store-tracker-client.tsx | 20 + src/app/dashboard/stores/page.tsx | 38 +- src/app/dashboard/stores/stores-client.tsx | 69 +++ src/app/dashboard/tiktok-shop/page.tsx | 20 +- .../tiktok-shop/tiktok-shop-client.tsx | 38 ++ src/app/dashboard/trends/page.tsx | 16 +- src/app/dashboard/trends/trends-client.tsx | 68 +++ src/app/layout.tsx | 8 +- src/app/login/page.tsx | 64 +-- src/app/page.tsx | 51 ++- src/app/pricing/page.tsx | 32 +- src/app/pricing/pricing-client.tsx | 36 ++ src/app/register/page.tsx | 66 +-- src/components/LangSwitch.tsx | 67 +++ src/components/ad-creative-media.tsx | 12 +- src/components/brand-tracker-actions.tsx | 6 +- src/components/checkout-button.tsx | 6 +- src/components/dashboard-mobile-nav.tsx | 4 +- src/components/magic-ai-runner.tsx | 8 +- src/components/store-tracker-manager.tsx | 20 +- src/components/tiktok-shop-import.tsx | 12 +- src/lib/i18n.ts | 394 ++++++++++++++++++ src/lib/use-lang.ts | 183 ++++++++ 39 files changed, 1393 insertions(+), 409 deletions(-) create mode 100644 src/app/dashboard/account/account-client.tsx create mode 100644 src/app/dashboard/admin/admin-client.tsx create mode 100644 src/app/dashboard/ads/ads-client.tsx create mode 100644 src/app/dashboard/brand-tracker/brand-tracker-client.tsx create mode 100644 src/app/dashboard/dashboard-layout-client.tsx create mode 100644 src/app/dashboard/magic-ai/magic-ai-client.tsx create mode 100644 src/app/dashboard/saved-ads/saved-ads-client.tsx create mode 100644 src/app/dashboard/store-tracker/store-tracker-client.tsx create mode 100644 src/app/dashboard/stores/stores-client.tsx create mode 100644 src/app/dashboard/tiktok-shop/tiktok-shop-client.tsx create mode 100644 src/app/dashboard/trends/trends-client.tsx create mode 100644 src/app/pricing/pricing-client.tsx create mode 100644 src/components/LangSwitch.tsx create mode 100644 src/lib/i18n.ts create mode 100644 src/lib/use-lang.ts 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")}
+
+
+
+ + + + +
+
+ {t("ads.advancedSearch")} +
+ + + + + + +
+
+
+
+ {["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
-
-
-
- - - - -
-
- Detaylı arama -
- - - - - - -
-
-
-
- {["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.

-
-
- - - -
- -
- - - - - - {stores.map((s) => ( - - - - - - - - - - ))} - -
Shop InfoBest SellersNicheMonthly VisitsEst. Revenue 30dMeta Ads
{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")}

+
+
+ + + +
+ +
+ + + + + + + + + + + + + + {stores.map((s) => ( + + + + + + + + + + ))} + +
{t("stores.shopInfo")}{t("stores.bestSellers")}{t("stores.niche")}{t("stores.monthlyVisits")}{t("stores.estRevenue")}{t("stores.metaAds")}
+
+ +
+
{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]) =>
{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]) =>
{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() {
-

Giriş yap

-

Hesabınızla veya Google üzerinden güvenli giriş yapın.

- - - setEmail(e.target.value)} +

{t("auth.login.title")}

+

{t("auth.login.subtitle")}

+ + + setEmail(e.target.value)} required /> - - - setPassword(e.target.value)} + + + setPassword(e.target.value)} required /> - + {error &&
{error}
} - Google ile devam et + {t("auth.googleContinue")} - +
- veya + {t("auth.or")}
- +
- Hesabınız yok mu? + {t("auth.noAccount")} - Hesap oluştur + {t("auth.createAccount")}
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 (