feat: wire remaining dashboard pages to i18n (pass 2+3)

This commit is contained in:
2026-08-25 20:11:00 +03:00
parent 931036fe62
commit f7861d6fd1
39 changed files with 1393 additions and 409 deletions
@@ -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 <div>
<h1 className="mb-6 text-3xl font-black">{t("account.title")}</h1>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<div className="text-sm text-slate-500">{t("account.account")}</div>
<div className="mt-2 font-black">{user.email}</div>
<div className="mt-1 text-xs text-slate-400">{t("account.role")}: {user.role}</div>
</Card>
<Card>
<div className="text-sm text-slate-500">{t("account.plan")}</div>
<div className="mt-2 text-2xl font-black">{user.subscription?.plan.name || "—"}</div>
<div className="mt-1 text-xs text-slate-400">{user.subscription?.status || t("account.noSubscription")}</div>
</Card>
<Card>
<div className="text-sm text-slate-500">{t("account.creditBalance")}</div>
<div className="mt-2 text-3xl font-black">{user.creditBalance.toLocaleString("tr-TR")}</div>
</Card>
</div>
</div>;
}
+2 -2
View File
@@ -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 <div><h1 className="mb-6 text-3xl font-black">Hesabım</h1><div className="grid gap-4 md:grid-cols-3"><Card><div className="text-sm text-slate-500">Hesap</div><div className="mt-2 font-black">{user.email}</div><div className="mt-1 text-xs text-slate-400">Yetki: {user.role}</div></Card><Card><div className="text-sm text-slate-500">Plan</div><div className="mt-2 text-2xl font-black">{user.subscription?.plan.name || "—"}</div><div className="mt-1 text-xs text-slate-400">{user.subscription?.status || "Abonelik yok"}</div></Card><Card><div className="text-sm text-slate-500">Kredi bakiyesi</div><div className="mt-2 text-3xl font-black">{user.creditBalance.toLocaleString("tr-TR")}</div></Card></div></div>;
return <AccountClient user={user} />;
}
+78
View File
@@ -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 (
<div>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3">
<div>
<div className="text-sm font-black uppercase tracking-[0.2em] text-violet-600">{t("admin.controlCenter")}</div>
<h1 className="mt-1 text-3xl font-black">{t("admin.title")}</h1>
<p className="mt-1 text-slate-500">{t("admin.description")}</p>
</div>
<div className="rounded-2xl bg-emerald-50 px-4 py-2 text-sm font-bold text-emerald-700">{t("admin.systemActive")}</div>
</div>
<AdminNav />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
<Card><div className="text-sm text-slate-500">{t("admin.totalUsers")}</div><div className="mt-2 text-3xl font-black">{users}</div><div className="mt-1 text-xs text-slate-400">{admins} {t("admin.admins")}</div></Card>
<Card><div className="text-sm text-slate-500">{t("admin.activeSubscriptions")}</div><div className="mt-2 text-3xl font-black">{activeSubscriptions}</div></Card>
<Card><div className="text-sm text-slate-500">{t("admin.revenue30d")}</div><div className="mt-2 text-3xl font-black">{money(paid._sum.amountCents)}</div><div className="mt-1 text-xs text-slate-400">{paid._count} {t("admin.payments")}</div></Card>
<Card><div className="text-sm text-slate-500">{t("admin.distributedCredits")}</div><div className="mt-2 text-3xl font-black">{(totalCredits._sum.creditBalance || 0).toLocaleString("tr-TR")}</div></Card>
<Card><div className="text-sm text-slate-500">{t("admin.quickAction")}</div><Link href="/dashboard/admin/users" className="mt-3 inline-block font-black text-violet-700">{t("admin.manageUsers")}</Link></Card>
</div>
<div className="mt-5 grid gap-5 xl:grid-cols-2">
<Card>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-black">{t("admin.recentPayments")}</h2>
<Link href="/dashboard/admin/payments" className="text-sm font-bold text-violet-700">{t("admin.viewAll")}</Link>
</div>
<div className="space-y-3">
{recentPayments.map((payment) => (
<div key={payment.id} className="flex items-center justify-between rounded-2xl bg-slate-50 p-3">
<div>
<div className="font-bold">{payment.user.email}</div>
<div className="text-xs text-slate-500">{payment.provider} · {payment.status}</div>
</div>
<div className="font-black">{money(payment.amountCents, payment.currency)}</div>
</div>
))}
{!recentPayments.length && <div className="text-sm text-slate-500">{t("admin.noPaymentsYet")}</div>}
</div>
</Card>
<Card>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-xl font-black">{t("admin.recentAdminActions")}</h2>
<Link href="/dashboard/admin/logs" className="text-sm font-bold text-violet-700">{t("admin.logCenter")}</Link>
</div>
<div className="space-y-3">
{recentAudits.map((log) => (
<div key={log.id} className="rounded-2xl border border-slate-100 p-3">
<div className="flex justify-between gap-3">
<b>{log.action}</b>
<span className="text-xs text-slate-400">{log.createdAt.toLocaleString("tr-TR")}</span>
</div>
<p className="mt-1 text-sm text-slate-600">{log.summary}</p>
<div className="mt-1 text-xs text-slate-400">{log.actorEmail}</div>
</div>
))}
{!recentAudits.length && <div className="text-sm text-slate-500">{t("admin.noAuditLogsYet")}</div>}
</div>
</Card>
</div>
</div>
);
}
+2 -28
View File
@@ -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 (
<div>
<div className="mb-6 flex flex-wrap items-end justify-between gap-3">
<div><div className="text-sm font-black uppercase tracking-[0.2em] text-violet-600">Kontrol Merkezi</div><h1 className="mt-1 text-3xl font-black">AdSeeQ Admin</h1><p className="mt-1 text-slate-500">Kullanıcı, gelir, kredi ve operasyon görünümü.</p></div>
<div className="rounded-2xl bg-emerald-50 px-4 py-2 text-sm font-bold text-emerald-700">Sistem aktif</div>
</div>
<AdminNav />
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
<Card><div className="text-sm text-slate-500">Toplam kullanıcı</div><div className="mt-2 text-3xl font-black">{users}</div><div className="mt-1 text-xs text-slate-400">{admins} admin</div></Card>
<Card><div className="text-sm text-slate-500">Aktif abonelik</div><div className="mt-2 text-3xl font-black">{activeSubscriptions}</div></Card>
<Card><div className="text-sm text-slate-500">30 gün gelir</div><div className="mt-2 text-3xl font-black">{money(paid._sum.amountCents)}</div><div className="mt-1 text-xs text-slate-400">{paid._count} ödeme</div></Card>
<Card><div className="text-sm text-slate-500">Dağıtılan kredi</div><div className="mt-2 text-3xl font-black">{(totalCredits._sum.creditBalance || 0).toLocaleString("tr-TR")}</div></Card>
<Card><div className="text-sm text-slate-500">Hızlı işlem</div><Link href="/dashboard/admin/users" className="mt-3 inline-block font-black text-violet-700">Kullanıcı yönet →</Link></Card>
</div>
<div className="mt-5 grid gap-5 xl:grid-cols-2">
<Card>
<div className="mb-4 flex items-center justify-between"><h2 className="text-xl font-black">Son ödemeler</h2><Link href="/dashboard/admin/payments" className="text-sm font-bold text-violet-700">Tümü</Link></div>
<div className="space-y-3">{recentPayments.map((payment) => <div key={payment.id} className="flex items-center justify-between rounded-2xl bg-slate-50 p-3"><div><div className="font-bold">{payment.user.email}</div><div className="text-xs text-slate-500">{payment.provider} · {payment.status}</div></div><div className="font-black">{money(payment.amountCents, payment.currency)}</div></div>)}{!recentPayments.length && <div className="text-sm text-slate-500">Henüz ödeme kaydı yok.</div>}</div>
</Card>
<Card>
<div className="mb-4 flex items-center justify-between"><h2 className="text-xl font-black">Son admin işlemleri</h2><Link href="/dashboard/admin/logs" className="text-sm font-bold text-violet-700">Log merkezi</Link></div>
<div className="space-y-3">{recentAudits.map((log) => <div key={log.id} className="rounded-2xl border border-slate-100 p-3"><div className="flex justify-between gap-3"><b>{log.action}</b><span className="text-xs text-slate-400">{log.createdAt.toLocaleString("tr-TR")}</span></div><p className="mt-1 text-sm text-slate-600">{log.summary}</p><div className="mt-1 text-xs text-slate-400">{log.actorEmail}</div></div>)}{!recentAudits.length && <div className="text-sm text-slate-500">Henüz audit kaydı yok.</div>}</div>
</Card>
</div>
</div>
);
return <AdminClient users={users} admins={admins} activeSubscriptions={activeSubscriptions} paid={paid} totalCredits={totalCredits} recentPayments={recentPayments} recentAudits={recentAudits} money={money} />;
}
+95
View File
@@ -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 (
<div>
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-black">Search Meta Adlibrary</h1>
<p className="mt-1 text-slate-500">{t("ads.description")}</p>
</div>
<div className="rounded-2xl bg-white px-4 py-3 text-sm shadow-sm">{t("ads.planLabel")}: <b>{plan?.name}</b> · {t("ads.planWarning")}</div>
</div>
<form className="mb-5 rounded-3xl bg-white p-4 shadow-soft">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-[1fr_220px_190px_140px]">
<input name="q" defaultValue={q} minLength={2} placeholder={t("ads.searchPlaceholder")} className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="country" defaultValue={country || "ALL"} aria-label={t("ads.country")} className="rounded-2xl border border-slate-200 px-4 py-3">
{AD_COUNTRIES.map(([code, label]) => <option key={code} value={code}>{label}</option>)}
</select>
<select name="mediaType" defaultValue={mediaType || ""} className="rounded-2xl border border-slate-200 px-4 py-3">
<option value="">{t("ads.allMedia")}</option><option>VIDEO</option><option>IMAGE</option><option>CAROUSEL</option>
</select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">{t("ads.search")}</button>
</div>
<details className="mt-3 rounded-2xl border border-slate-100 bg-slate-50/70 px-4 py-3" open={Boolean(niche || minDays || language || status !== "ACTIVE" || matchMode !== "ALL_WORDS" || sort !== "relevance")}>
<summary className="cursor-pointer select-none text-sm font-black text-slate-700">{t("ads.advancedSearch")}</summary>
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-6">
<select name="matchMode" defaultValue={matchMode as any} aria-label={t("ads.matchMode")} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="ALL_WORDS">{t("ads.allWords")}</option><option value="EXACT_PHRASE">{t("ads.exactPhrase")}</option>
</select>
<select name="status" defaultValue={status as any} aria-label={t("ads.adStatus")} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="ACTIVE">{t("ads.activeAds")}</option><option value="INACTIVE">{t("ads.inactiveAds")}</option><option value="ALL">{t("ads.allStatuses")}</option>
</select>
<select name="minDays" defaultValue={String(minDays)} aria-label={t("ads.minRunTime")} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="0">{t("ads.allDurations")}</option><option value="7">{t("ads.minDays", { days: 7 })}</option><option value="30">{t("ads.minDays", { days: 30 })}</option><option value="90">{t("ads.minDays", { days: 90 })}</option>
</select>
<select name="niche" defaultValue={niche || ""} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="">{t("ads.allNiches")}</option><option>Pets</option><option>Beauty</option><option>Supplements</option><option>Household</option>
</select>
<select name="language" defaultValue={language || ""} aria-label={t("ads.language")} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="">{t("ads.allLanguages")}</option><option value="tr">{t("ads.turkish")}</option><option value="en">{t("ads.english")}</option><option value="de">{t("ads.german")}</option><option value="fr">{t("ads.french")}</option><option value="es">{t("ads.spanish")}</option>
</select>
<select name="sort" defaultValue={sort} aria-label={t("ads.sortBy")} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="relevance">{t("ads.mostRelevant")}</option><option value="newest">{t("ads.newest")}</option><option value="longest">{t("ads.longestRunning")}</option>
</select>
</div>
</details>
</form>
<div className="mb-5 flex flex-wrap gap-2">
{["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => <span key={x} className="rounded-full bg-violet-50 px-3 py-1 text-sm font-semibold text-violet-800">{x}</span>)}
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{q && <ApifyEmptySearch query={q} country={country || "ALL"} mediaType={mediaType || "ALL"} matchMode={matchMode as any} status={status as any} planLimit={apifyPlanLimit} existingCount={ads.length} />}
{ads.map((ad: any) => (
<Card key={ad.id} className="relative overflow-hidden">
{ad.isLocked && <div className="absolute inset-0 z-10 grid place-items-center bg-white/70 backdrop-blur-[2px]"><Link href="/pricing" className="rounded-2xl bg-violet-700 px-5 py-3 font-black text-white">{t("ads.unlockWinners")}</Link></div>}
<div className={ad.isLocked ? "locked-blur" : ""}>
<AdCreativeMedia creative={ad.creatives[0]} className="mb-4 h-44 w-full rounded-2xl object-cover" />
<div className="mb-2 flex items-center justify-between">
<div className="font-black">{ad.headline}</div>
<div className="rounded-full bg-emerald-50 px-2 py-1 text-xs font-bold text-emerald-700">Top %{Math.round(ad.rankPercentile || 0)}</div>
</div>
<div className="text-sm font-semibold text-slate-500">{ad.brandPage?.name} · {ad.mediaType} · {ad.daysRunning} {t("ads.days")}</div>
<p className="mt-3 line-clamp-3 text-sm text-slate-600">{ad.primaryText}</p>
<div className="mt-4 grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-xl bg-slate-50 p-2"><b>{ad.countries.join(", ")}</b><br />{t("ads.country")}</div>
<div className="rounded-xl bg-slate-50 p-2"><b>€{ad.estimatedSpendMin ?? "—"}</b><br />{t("ads.minSpend")}</div>
<div className="rounded-xl bg-slate-50 p-2"><b>{ad.adScore}</b><br />{t("ads.score")}</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
}
+2 -71
View File
@@ -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<Record<string, string | undefined>> }) {
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 (
<div>
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-black">Search Meta Adlibrary</h1>
<p className="mt-1 text-slate-500">Kazanan Meta reklamlarını anahtar kelime, ülke, niş ve medya tipine göre keşfet.</p>
</div>
<div className="rounded-2xl bg-white px-4 py-3 text-sm shadow-sm">Plan: <b>{plan?.name}</b> · Free ise kartlar kilitli</div>
</div>
<form className="mb-5 rounded-3xl bg-white p-4 shadow-soft">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-[1fr_220px_190px_140px]">
<input name="q" defaultValue={q} minLength={2} placeholder="Marka, ürün veya anahtar kelime..." className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="country" defaultValue={country || "ALL"} aria-label="Ülke" className="rounded-2xl border border-slate-200 px-4 py-3">
{AD_COUNTRIES.map(([code, label]) => <option key={code} value={code}>{label}</option>)}
</select>
<select name="mediaType" defaultValue={mediaType || ""} className="rounded-2xl border border-slate-200 px-4 py-3">
<option value="">Tüm medya</option><option>VIDEO</option><option>IMAGE</option><option>CAROUSEL</option>
</select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">Ara</button>
</div>
<details className="mt-3 rounded-2xl border border-slate-100 bg-slate-50/70 px-4 py-3" open={Boolean(niche || minDays || language || status !== "ACTIVE" || matchMode !== "ALL_WORDS" || sort !== "relevance")}>
<summary className="cursor-pointer select-none text-sm font-black text-slate-700">Detaylı arama</summary>
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-6">
<select name="matchMode" defaultValue={matchMode} aria-label="Kelime eşleşmesi" className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="ALL_WORDS">Tüm kelimeler</option><option value="EXACT_PHRASE">Tam ifade</option>
</select>
<select name="status" defaultValue={status} aria-label="Reklam durumu" className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="ACTIVE">Aktif reklamlar</option><option value="INACTIVE">Pasif reklamlar</option><option value="ALL">Tüm durumlar</option>
</select>
<select name="minDays" defaultValue={String(minDays)} aria-label="Minimum yayın süresi" className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="0">Tüm süreler</option><option value="7">En az 7 gün</option><option value="30">En az 30 gün</option><option value="90">En az 90 gün</option>
</select>
<select name="niche" defaultValue={niche || ""} className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="">Tüm nişler</option><option>Pets</option><option>Beauty</option><option>Supplements</option><option>Household</option>
</select>
<select name="language" defaultValue={language || ""} aria-label="Dil" className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="">Tüm diller</option><option value="tr">Türkçe</option><option value="en">İngilizce</option><option value="de">Almanca</option><option value="fr">Fransızca</option><option value="es">İspanyolca</option>
</select>
<select name="sort" defaultValue={sort} aria-label="Sıralama" className="rounded-xl border border-slate-200 bg-white px-3 py-3 text-sm">
<option value="relevance">En ilgili</option><option value="newest">En yeni</option><option value="longest">En uzun süren</option>
</select>
</div>
</details>
</form>
<div className="mb-5 flex flex-wrap gap-2">
{["Week's biggest winners", "US winners", "Dropship Ads", "Supplements", "Top Branded"].map((x) => <span key={x} className="rounded-full bg-violet-50 px-3 py-1 text-sm font-semibold text-violet-800">{x}</span>)}
</div>
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{q && <ApifyEmptySearch query={q} country={country || "ALL"} mediaType={mediaType || "ALL"} matchMode={matchMode} status={status} planLimit={apifyPlanLimit} existingCount={masked.length} />}
{masked.map((ad: any) => (
<Card key={ad.id} className="relative overflow-hidden">
{ad.isLocked && <div className="absolute inset-0 z-10 grid place-items-center bg-white/70 backdrop-blur-[2px]"><Link href="/pricing" className="rounded-2xl bg-violet-700 px-5 py-3 font-black text-white">Start now — Unlock winners</Link></div>}
<div className={ad.isLocked ? "locked-blur" : ""}>
<AdCreativeMedia creative={ad.creatives[0]} className="mb-4 h-44 w-full rounded-2xl object-cover" />
<div className="mb-2 flex items-center justify-between">
<div className="font-black">{ad.headline}</div>
<div className="rounded-full bg-emerald-50 px-2 py-1 text-xs font-bold text-emerald-700">Top %{Math.round(ad.rankPercentile || 0)}</div>
</div>
<div className="text-sm font-semibold text-slate-500">{ad.brandPage?.name} · {ad.mediaType} · {ad.daysRunning} gün</div>
<p className="mt-3 line-clamp-3 text-sm text-slate-600">{ad.primaryText}</p>
<div className="mt-4 grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-xl bg-slate-50 p-2"><b>{ad.countries.join(", ")}</b><br />Ülke</div>
<div className="rounded-xl bg-slate-50 p-2"><b>€{ad.estimatedSpendMin ?? "—"}</b><br />Min spend</div>
<div className="rounded-xl bg-slate-50 p-2"><b>{ad.adScore}</b><br />Score</div>
</div>
</div>
</Card>
))}
</div>
</div>
);
return <AdsClient plan={plan} q={q} niche={niche} mediaType={mediaType} country={country} status={status} matchMode={matchMode} minDays={minDays} language={language} sort={sort} ads={masked} apifyPlanLimit={apifyPlanLimit} />;
}
@@ -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<any, any>;
limit: number | null;
}) {
const t = useT();
return (
<div>
<div className="mb-6 flex flex-wrap items-end justify-between gap-4">
<div><h1 className="text-3xl font-black">{t("brand.title")}</h1><p className="mt-1 text-slate-500">{t("brand.description")}</p></div>
<div className="rounded-2xl bg-white px-4 py-3 text-sm shadow-sm"><b>{tracked.length}</b> {t("brand.trackingLabel")} · {t("brand.limitLabel")}: <b>{limit === null ? t("brand.unlimited") : limit}</b></div>
</div>
<form className="mb-5 flex gap-3 rounded-3xl bg-white p-4 shadow-soft">
<input name="q" defaultValue={query} placeholder={t("brand.searchPlaceholder")} className="min-w-0 flex-1 rounded-2xl border border-slate-200 px-4 py-3" />
<button className="rounded-2xl bg-slate-950 px-5 py-3 font-bold text-white">{t("brand.search")}</button>
</form>
{alerts.length > 0 && <Card className="mb-5"><h2 className="text-lg font-black">{t("brand.newAlerts")}</h2><div className="mt-3 space-y-2">{alerts.map((alert) => <div key={alert.id} className="flex items-center justify-between gap-3 rounded-2xl bg-violet-50 p-3 text-sm"><div><b>{alert.trackedBrand.brandPage.name}</b><p className="text-slate-600">{alert.title}</p></div><span className="shrink-0 text-xs text-slate-400">{alert.createdAt.toLocaleDateString("tr-TR")}</span></div>)}</div></Card>}
<div className="grid gap-4 lg:grid-cols-2">
{brands.map((brand) => {
const activeAds = brand.ads.filter((ad: any) => ad.status === "ACTIVE").length;
return <Card key={brand.id} className="flex flex-col gap-4">
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-violet-100 font-black text-violet-700">{brand.name.slice(0, 2).toUpperCase()}</div>
<div className="min-w-0"><h2 className="truncate text-lg font-black">{brand.name}</h2><p className="truncate text-sm text-slate-500">{brand.websiteDomain || brand.pageUrl || t("brand.metaPage")}</p></div>
</div>
<BrandTrackerActions brandPageId={brand.id} trackingId={trackedByBrand.get(brand.id)} />
</div>
<div className="grid grid-cols-3 gap-2 text-center text-sm">
<div className="rounded-2xl bg-slate-50 p-3"><b>{brand._count.ads}</b><br /><span className="text-xs text-slate-500">{t("brand.totalAds")}</span></div>
<div className="rounded-2xl bg-emerald-50 p-3"><b>{activeAds}</b><br /><span className="text-xs text-slate-500">{t("brand.recentActive")}</span></div>
<div className="rounded-2xl bg-slate-50 p-3"><b>{brand.niche || "—"}</b><br /><span className="text-xs text-slate-500">{t("brand.niche")}</span></div>
</div>
<div className="space-y-2">
{brand.ads.map((ad: any) => <div key={ad.id} className="flex items-center justify-between gap-3 rounded-xl border border-slate-100 p-3 text-sm"><span className="truncate font-semibold">{ad.headline || t("brand.headlessCreative")}</span><span className="shrink-0 text-xs text-slate-500">{ad.mediaType} · {ad.daysRunning || "—"} {t("brand.days")}</span></div>)}
{!brand.ads.length && <p className="text-sm text-slate-500">{t("brand.noAdsYet")}</p>}
</div>
</Card>;
})}
{!brands.length && <Card className="text-center text-slate-500 lg:col-span-2">{t("brand.noResults")}</Card>}
</div>
</div>
);
}
+2 -32
View File
@@ -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<Record<string, string | undefined>> }) {
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 (
<div>
<div className="mb-6 flex flex-wrap items-end justify-between gap-4">
<div><h1 className="text-3xl font-black">Brand Tracker</h1><p className="mt-1 text-slate-500">Markaları takip et; aktif reklam sayısını ve son kreatiflerini tek ekranda izle.</p></div>
<div className="rounded-2xl bg-white px-4 py-3 text-sm shadow-sm"><b>{tracked.length}</b> takip · Limit: <b>{limit === null ? "Sınırsız" : limit}</b></div>
</div>
<form className="mb-5 flex gap-3 rounded-3xl bg-white p-4 shadow-soft">
<input name="q" defaultValue={query} placeholder="Marka, domain veya niche ara" className="min-w-0 flex-1 rounded-2xl border border-slate-200 px-4 py-3" />
<button className="rounded-2xl bg-slate-950 px-5 py-3 font-bold text-white">Ara</button>
</form>
{alerts.length > 0 && <Card className="mb-5"><h2 className="text-lg font-black">Yeni reklam uyarıları</h2><div className="mt-3 space-y-2">{alerts.map((alert) => <div key={alert.id} className="flex items-center justify-between gap-3 rounded-2xl bg-violet-50 p-3 text-sm"><div><b>{alert.trackedBrand.brandPage.name}</b><p className="text-slate-600">{alert.title}</p></div><span className="shrink-0 text-xs text-slate-400">{alert.createdAt.toLocaleDateString("tr-TR")}</span></div>)}</div></Card>}
<div className="grid gap-4 lg:grid-cols-2">
{brands.map((brand) => {
const activeAds = brand.ads.filter((ad) => ad.status === "ACTIVE").length;
return <Card key={brand.id} className="flex flex-col gap-4">
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-center gap-3">
<div className="grid h-12 w-12 shrink-0 place-items-center rounded-2xl bg-violet-100 font-black text-violet-700">{brand.name.slice(0, 2).toUpperCase()}</div>
<div className="min-w-0"><h2 className="truncate text-lg font-black">{brand.name}</h2><p className="truncate text-sm text-slate-500">{brand.websiteDomain || brand.pageUrl || "Meta marka sayfası"}</p></div>
</div>
<BrandTrackerActions brandPageId={brand.id} trackingId={trackedByBrand.get(brand.id)} />
</div>
<div className="grid grid-cols-3 gap-2 text-center text-sm"><div className="rounded-2xl bg-slate-50 p-3"><b>{brand._count.ads}</b><br /><span className="text-xs text-slate-500">Toplam reklam</span></div><div className="rounded-2xl bg-emerald-50 p-3"><b>{activeAds}</b><br /><span className="text-xs text-slate-500">Son 3 aktif</span></div><div className="rounded-2xl bg-slate-50 p-3"><b>{brand.niche || "—"}</b><br /><span className="text-xs text-slate-500">Niche</span></div></div>
<div className="space-y-2">{brand.ads.map((ad) => <div key={ad.id} className="flex items-center justify-between gap-3 rounded-xl border border-slate-100 p-3 text-sm"><span className="truncate font-semibold">{ad.headline || "Başlıksız kreatif"}</span><span className="shrink-0 text-xs text-slate-500">{ad.mediaType} · {ad.daysRunning || "—"} gün</span></div>)}{!brand.ads.length && <p className="text-sm text-slate-500">Henüz reklam verisi yok.</p>}</div>
</Card>;
})}
{!brands.length && <Card className="text-center text-slate-500 lg:col-span-2">Aramanızla eşleşen marka bulunamadı.</Card>}
</div>
</div>
);
return <BrandTrackerClient query={query} brands={brands} tracked={tracked} alerts={alerts} trackedByBrand={trackedByBrand} limit={limit} />;
}
@@ -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 (
<>
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/90 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/dashboard/ads"><BrandLogo markClassName="h-8 w-8" /></Link>
<DashboardMobileNav isAdmin={user.role === "ADMIN"} />
<nav className="hidden gap-1 lg:flex">
{nav.map(([label, href]) => (
<Link key={label} href={href} className="rounded-xl px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100">
{label}
</Link>
))}
{user.role === "ADMIN" && <Link href="/dashboard/admin" className="rounded-xl bg-violet-50 px-3 py-2 text-sm font-black text-violet-700 hover:bg-violet-100">{t("nav.admin")}</Link>}
</nav>
<div className="flex items-center gap-3">
{user.role !== "ADMIN" && <Link href="/pricing" className="hidden rounded-xl bg-violet-700 px-4 py-2 text-sm font-bold text-white sm:block">{t("nav.upgrade")}</Link>}
<div className="text-right text-sm">
<div className="font-bold">{user.name || user.email}</div>
<div className="text-xs text-slate-500">{user.subscription?.plan.name}</div>
</div>
</div>
</div>
</header>
<main className="mx-auto max-w-7xl px-4 py-8">{children}</main>
</>
);
}
+5 -24
View File
@@ -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 (
<div className="min-h-screen bg-slate-50">
<header className="sticky top-0 z-30 border-b border-slate-200 bg-white/90 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
<Link href="/dashboard/ads"><BrandLogo markClassName="h-8 w-8" /></Link>
<DashboardMobileNav isAdmin={user.role === "ADMIN"} />
<nav className="hidden gap-1 lg:flex">
{nav.map(([label, href]) => (
<Link key={label} href={href} className="rounded-xl px-3 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-100">
{label}
</Link>
))}
{user.role === "ADMIN" && <Link href="/dashboard/admin" className="rounded-xl bg-violet-50 px-3 py-2 text-sm font-black text-violet-700 hover:bg-violet-100">Admin</Link>}
</nav>
<div className="flex items-center gap-3">
{user.role !== "ADMIN" && <Link href="/pricing" className="hidden rounded-xl bg-violet-700 px-4 py-2 text-sm font-bold text-white sm:block">Upgrade</Link>}
<div className="text-right text-sm">
<div className="font-bold">{user.name || user.email}</div>
<div className="text-xs text-slate-500">{user.subscription?.plan.name}</div>
</div>
</div>
</div>
</header>
<main className="mx-auto max-w-7xl px-4 py-8">{children}</main>
<DashboardLayoutClient user={user} nav={nav}>
{children}
</DashboardLayoutClient>
</div>
);
}
@@ -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 (
<div>
<div className="mb-6"><h1 className="text-3xl font-black">{t("ai.magic")}</h1><p className="mt-1 text-slate-500">{t("ai.description")}</p></div>
<MagicAiRunner />
{analyses.length > 0 && <div className="mb-5 grid gap-4 lg:grid-cols-2">{analyses.map((analysis) => <Card key={analysis.id}><div className="flex justify-between gap-3"><h2 className="font-black">{analysis.query || t("ai.generalAnalysis")}</h2><span className="text-xs text-slate-400">{analysis.model}</span></div><p className="mt-2 text-sm text-slate-600">{analysis.summary}</p><div className="mt-3 space-y-2">{(analysis.insights as Array<{title:string;action:string;confidence:number}>).map((item, index) => <div key={index} className="rounded-2xl bg-violet-50 p-3 text-sm"><b>{item.title}</b><p className="mt-1 text-slate-600">{item.action}</p></div>)}</div></Card>)}</div>}
<div className="mb-5 rounded-3xl bg-gradient-to-r from-violet-700 to-fuchsia-600 p-6 text-white"><div className="text-sm font-bold uppercase tracking-widest text-violet-100">Creative Intelligence</div><h2 className="mt-2 text-2xl font-black">{ads.length} {t("ai.actionPlanTitle")}</h2><p className="mt-1 text-violet-100">{t("ai.actionPlanDescription")}</p></div>
<div className="grid gap-5 lg:grid-cols-2">{ads.map((ad) => { const insight = recommendation(ad); return <Card key={ad.id}>
<div className="grid gap-4 sm:grid-cols-[180px_1fr]"><AdCreativeMedia creative={ad.creatives[0]} className="h-44 w-full rounded-2xl object-cover" /><div><div className="flex items-start justify-between gap-3"><div><h2 className="font-black">{ad.headline || t("ai.headlessAd")}</h2><p className="text-sm text-slate-500">{ad.brandPage?.name || t("ai.unknownBrand")} · {ad.mediaType} · {ad.daysRunning || "—"} {t("ai.days")}</p></div><span className={`rounded-full px-3 py-1 text-xs font-black ${insight.color}`}>{insight.label}</span></div><div className="mt-4 rounded-2xl bg-slate-50 p-3"><div className="text-xs font-bold uppercase tracking-wide text-slate-400">{t("ai.hook")}</div><p className="mt-1 line-clamp-2 text-sm font-semibold">{sentence(ad.primaryText)}</p></div><p className="mt-3 text-sm text-slate-600">{insight.text}</p></div></div>
</Card>; })}{!ads.length && <Card className="text-center text-slate-500 lg:col-span-2">{t("ai.noAdsToAnalyze")}</Card>}</div>
</div>
);
}
+2 -14
View File
@@ -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 (
<div>
<div className="mb-6"><h1 className="text-3xl font-black">Magic AI</h1><p className="mt-1 text-slate-500">Kreatif dayanıklılığı, formatı ve reklam metnini analiz ederek uygulanabilir test önerileri üretir.</p></div>
<MagicAiRunner />
{analyses.length > 0 && <div className="mb-5 grid gap-4 lg:grid-cols-2">{analyses.map((analysis) => <Card key={analysis.id}><div className="flex justify-between gap-3"><h2 className="font-black">{analysis.query || "Genel kreatif analizi"}</h2><span className="text-xs text-slate-400">{analysis.model}</span></div><p className="mt-2 text-sm text-slate-600">{analysis.summary}</p><div className="mt-3 space-y-2">{(analysis.insights as Array<{title:string;action:string;confidence:number}>).map((item, index) => <div key={index} className="rounded-2xl bg-violet-50 p-3 text-sm"><b>{item.title}</b><p className="mt-1 text-slate-600">{item.action}</p></div>)}</div></Card>)}</div>}
<div className="mb-5 rounded-3xl bg-gradient-to-r from-violet-700 to-fuchsia-600 p-6 text-white"><div className="text-sm font-bold uppercase tracking-widest text-violet-100">Creative Intelligence</div><h2 className="mt-2 text-2xl font-black">{ads.length} reklamdan aksiyon planı</h2><p className="mt-1 text-violet-100">Öneriler canlı reklam süresi ve mevcut kreatif sinyallerinden hesaplanır.</p></div>
<div className="grid gap-5 lg:grid-cols-2">{ads.map((ad) => { const insight = recommendation(ad); return <Card key={ad.id}>
<div className="grid gap-4 sm:grid-cols-[180px_1fr]"><AdCreativeMedia creative={ad.creatives[0]} className="h-44 w-full rounded-2xl object-cover" /><div><div className="flex items-start justify-between gap-3"><div><h2 className="font-black">{ad.headline || "Başlıksız reklam"}</h2><p className="text-sm text-slate-500">{ad.brandPage?.name || "Bilinmeyen marka"} · {ad.mediaType} · {ad.daysRunning || "—"} gün</p></div><span className={`rounded-full px-3 py-1 text-xs font-black ${insight.color}`}>{insight.label}</span></div><div className="mt-4 rounded-2xl bg-slate-50 p-3"><div className="text-xs font-bold uppercase tracking-wide text-slate-400">Kanca</div><p className="mt-1 line-clamp-2 text-sm font-semibold">{sentence(ad.primaryText)}</p></div><p className="mt-3 text-sm text-slate-600">{insight.text}</p></div></div>
</Card>; })}{!ads.length && <Card className="text-center text-slate-500 lg:col-span-2">Analiz edilecek reklam bulunamadı.</Card>}</div>
</div>
);
return <MagicAiClient q={q} ads={ads} analyses={analyses} sentence={sentence} recommendation={recommendation} />;
}
+2 -16
View File
@@ -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 (
<div>
<h1 className="mb-6 text-3xl font-black">Saved Ads</h1>
<div className="grid gap-5 lg:grid-cols-[260px_1fr]">
<Card>
<h2 className="mb-3 font-black">Folders</h2>
<div className="space-y-2"><div className="rounded-xl bg-violet-50 p-3 font-bold text-violet-700">All Saved Ads ({saved.length})</div>{folders.map((f) => <div key={f.id} className="rounded-xl bg-slate-50 p-3 font-bold">{f.name}</div>)}</div>
</Card>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{saved.map((s) => <Card key={s.id}><AdCreativeMedia creative={s.ad.creatives[0]} className="mb-4 h-40 w-full rounded-2xl object-cover" /><div className="font-black">{s.ad.headline}</div><div className="text-sm text-slate-500">{s.ad.brandPage?.name} · {s.folder?.name || "All"}</div><p className="mt-2 line-clamp-2 text-sm">{s.ad.primaryText}</p></Card>)}
</div>
</div>
</div>
);
return <SavedAdsClient folders={folders} saved={saved} />;
}
@@ -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 (
<div>
<h1 className="mb-6 text-3xl font-black">{t("savedAds.title")}</h1>
<div className="grid gap-5 lg:grid-cols-[260px_1fr]">
<Card>
<h2 className="mb-3 font-black">{t("savedAds.folders")}</h2>
<div className="space-y-2">
<div className="rounded-xl bg-violet-50 p-3 font-bold text-violet-700">{t("savedAds.allAds")} ({saved.length})</div>
{folders.map((f) => <div key={f.id} className="rounded-xl bg-slate-50 p-3 font-bold">{f.name}</div>)}
</div>
</Card>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{saved.map((s) => (
<Card key={s.id}>
<AdCreativeMedia creative={s.ad.creatives[0]} className="mb-4 h-40 w-full rounded-2xl object-cover" />
<div className="font-black">{s.ad.headline}</div>
<div className="text-sm text-slate-500">{s.ad.brandPage?.name} · {s.folder?.name || t("savedAds.all")}</div>
<p className="mt-2 line-clamp-2 text-sm">{s.ad.primaryText}</p>
</Card>
))}
</div>
</div>
</div>
);
}
+2 -9
View File
@@ -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 (
<div>
<h1 className="text-3xl font-black">Store Tracker</h1>
<p className="mt-1 text-slate-500">Plan limitine göre rakip Shopify mağazalarını watchlist’e ekle.</p>
<Card className="mt-5">
<StoreTrackerManager tracked={tracked} limit={limit} />
</Card>
</div>
);
return <StoreTrackerClient tracked={tracked} limit={limit} />;
}
@@ -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 (
<div>
<h1 className="text-3xl font-black">{t("store.title")}</h1>
<p className="mt-1 text-slate-500">{t("store.description")}</p>
<Card className="mt-5">
<StoreTrackerManager tracked={tracked} limit={limit} />
</Card>
</div>
);
}
+2 -36
View File
@@ -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<Record<string, string | undefined>> }) {
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 (
<div>
<div className="mb-6">
<h1 className="text-3xl font-black">Explore Stores</h1>
<p className="mt-1 text-slate-500">Shopify mağazalarını trafik, gelir, niche ve aktif reklam sayısıyla keşfet.</p>
</div>
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-[1fr_220px_120px]">
<input name="q" defaultValue={q} placeholder="petpro, beauty, supplements..." className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="niche" defaultValue={niche || ""} className="rounded-2xl border border-slate-200 px-4 py-3"><option value="">Tüm niche</option><option>Pets</option><option>Beauty</option><option>Supplements</option><option>Household</option></select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">Ara</button>
</form>
<Card className="overflow-hidden p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px] text-left text-sm">
<thead className="bg-slate-50 text-xs uppercase text-slate-500">
<tr><th className="p-4">Shop Info</th><th>Best Sellers</th><th>Niche</th><th>Monthly Visits</th><th>Est. Revenue 30d</th><th>Meta Ads</th><th></th></tr>
</thead>
<tbody>
{stores.map((s) => (
<tr key={s.id} className="border-t border-slate-100">
<td className="p-4"><div className="flex items-center gap-3"><img src={s.logoUrl || ""} className="h-10 w-10 rounded-xl" /><div><div className="font-black">{s.name}</div><div className="text-slate-500">{s.domain} · {s.country}</div></div></div></td>
<td>{s.products.map((p) => p.title).join(", ")}</td>
<td>{s.niche}</td>
<td>{s.monthlyVisits?.toLocaleString()} <span className="text-emerald-600">+{s.monthlyVisitGrowth}%</span></td>
<td>€{s.estRevenue30dMin?.toLocaleString()}–€{s.estRevenue30dMax?.toLocaleString()}</td>
<td>{s.brandPages[0]?._count.ads || 0}</td>
<td><Link href={`/dashboard/stores/${s.id}`} className="font-bold text-violet-700">Details</Link></td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
);
return <StoresClient q={q} niche={niche} stores={stores} />;
}
@@ -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 (
<div>
<div className="mb-6">
<h1 className="text-3xl font-black">{t("stores.title")}</h1>
<p className="mt-1 text-slate-500">{t("stores.description")}</p>
</div>
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-[1fr_220px_120px]">
<input name="q" defaultValue={q} placeholder={t("stores.searchPlaceholder")} className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="niche" defaultValue={niche || ""} className="rounded-2xl border border-slate-200 px-4 py-3">
<option value="">{t("stores.allNiches")}</option>
<option>Pets</option>
<option>Beauty</option>
<option>Supplements</option>
<option>Household</option>
</select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">{t("stores.search")}</button>
</form>
<Card className="overflow-hidden p-0">
<div className="overflow-x-auto">
<table className="w-full min-w-[900px] text-left text-sm">
<thead className="bg-slate-50 text-xs uppercase text-slate-500">
<tr>
<th className="p-4">{t("stores.shopInfo")}</th>
<th>{t("stores.bestSellers")}</th>
<th>{t("stores.niche")}</th>
<th>{t("stores.monthlyVisits")}</th>
<th>{t("stores.estRevenue")}</th>
<th>{t("stores.metaAds")}</th>
<th></th>
</tr>
</thead>
<tbody>
{stores.map((s) => (
<tr key={s.id} className="border-t border-slate-100">
<td className="p-4">
<div className="flex items-center gap-3">
<img src={s.logoUrl || ""} className="h-10 w-10 rounded-xl" />
<div>
<div className="font-black">{s.name}</div>
<div className="text-slate-500">{s.domain} · {s.country}</div>
</div>
</div>
</td>
<td>{s.products.map((p: any) => p.title).join(", ")}</td>
<td>{s.niche}</td>
<td>{s.monthlyVisits?.toLocaleString()} <span className="text-emerald-600">+{s.monthlyVisitGrowth}%</span></td>
<td>€{s.estRevenue30dMin?.toLocaleString()}–€{s.estRevenue30dMax?.toLocaleString()}</td>
<td>{s.brandPages[0]?._count.ads || 0}</td>
<td><Link href={`/dashboard/stores/${s.id}`} className="font-bold text-violet-700">{t("stores.details")}</Link></td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
</div>
);
}
+2 -18
View File
@@ -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 <div>
<div className="mb-6"><h1 className="text-3xl font-black">TikTok Shop</h1><p className="mt-1 text-slate-500">Canlı ürün, satış, fiyat, mağaza ve değerlendirme sinyalleri.</p></div>
<TikTokShopImport planLimit={planLimit} />
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-[1fr_180px_120px]">
<input name="q" defaultValue={q} placeholder="Kayıtlı ürün veya mağaza ara" className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="region" defaultValue={region || ""} className="rounded-2xl border border-slate-200 px-4 py-3"><option value="">Tüm bölgeler</option>{regions.map((item) => <option key={item}>{item}</option>)}</select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">Filtrele</button>
</form>
<div className="mb-5 grid gap-4 sm:grid-cols-3"><Card><div className="text-sm text-slate-500">Canlı ürün</div><div className="mt-1 text-3xl font-black">{ranked.length}</div></Card><Card><div className="text-sm text-slate-500">Mağaza</div><div className="mt-1 text-3xl font-black">{new Set(ranked.map((item) => item.shopExternalId || item.shopName)).size}</div></Card><Card><div className="text-sm text-slate-500">Toplam satış sinyali</div><div className="mt-1 text-3xl font-black">{ranked.reduce((sum, item) => sum + (item.soldCount || 0), 0).toLocaleString("tr-TR")}</div></Card></div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{ranked.map((product) => <Card key={product.id}>
{product.imageUrl ? <img src={product.imageUrl} alt="" className="mb-4 h-44 w-full rounded-2xl object-cover" /> : <div className="mb-4 grid h-44 place-items-center rounded-2xl bg-slate-100 text-slate-400">Ürün görseli yok</div>}
<div className="flex items-start justify-between gap-3"><div><h2 className="font-black">{product.title}</h2><p className="text-sm text-slate-500">{product.shopName || "Mağaza bilinmiyor"} · {product.region}</p></div><span className="rounded-full bg-fuchsia-50 px-3 py-1 text-sm font-black text-fuchsia-700">{product.score}</span></div>
<div className="mt-4 grid grid-cols-3 gap-2 text-center text-xs"><div className="rounded-xl bg-slate-50 p-2"><b>{product.price ? `${product.price} ${product.currency || ""}` : "—"}</b><br />Fiyat</div><div className="rounded-xl bg-slate-50 p-2"><b>{(product.soldCount || 0).toLocaleString("tr-TR")}</b><br />Satış</div><div className="rounded-xl bg-slate-50 p-2"><b>{product.rating || "—"}</b><br />Puan</div></div>
{product.productUrl && <a href={product.productUrl} target="_blank" rel="noreferrer" className="mt-4 inline-block text-sm font-bold text-violet-700">TikTok’ta aç →</a>}
</Card>)}{!ranked.length && <Card className="text-center text-slate-500 md:col-span-2 xl:col-span-3">Yukarıdan bir ürün aratıp canlı veriyi getirin.</Card>}</div>
</div>;
return <TikTokShopClient planLimit={planLimit} q={q} region={region} ranked={ranked} regions={regions} />;
}
@@ -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 <div>
<div className="mb-6"><h1 className="text-3xl font-black">{t("tiktok.title")}</h1><p className="mt-1 text-slate-500">{t("tiktok.description")}</p></div>
<TikTokShopImport planLimit={planLimit} />
<form className="mb-5 grid gap-3 rounded-3xl bg-white p-4 shadow-soft md:grid-cols-[1fr_180px_120px]">
<input name="q" defaultValue={q} placeholder={t("tiktok.searchPlaceholder")} className="rounded-2xl border border-slate-200 px-4 py-3" />
<select name="region" defaultValue={region || ""} className="rounded-2xl border border-slate-200 px-4 py-3"><option value="">{t("tiktok.allRegions")}</option>{regions.map((item) => <option key={item}>{item}</option>)}</select>
<button className="rounded-2xl bg-slate-950 px-4 py-3 font-bold text-white">{t("tiktok.filter")}</button>
</form>
<div className="mb-5 grid gap-4 sm:grid-cols-3">
<Card><div className="text-sm text-slate-500">{t("tiktok.liveProducts")}</div><div className="mt-1 text-3xl font-black">{ranked.length}</div></Card>
<Card><div className="text-sm text-slate-500">{t("tiktok.stores")}</div><div className="mt-1 text-3xl font-black">{new Set(ranked.map((item) => item.shopExternalId || item.shopName)).size}</div></Card>
<Card><div className="text-sm text-slate-500">{t("tiktok.totalSalesSignal")}</div><div className="mt-1 text-3xl font-black">{ranked.reduce((sum, item) => sum + (item.soldCount || 0), 0).toLocaleString("tr-TR")}</div></Card>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{ranked.map((product) => <Card key={product.id}>
{product.imageUrl ? <img src={product.imageUrl} alt="" className="mb-4 h-44 w-full rounded-2xl object-cover" /> : <div className="mb-4 grid h-44 place-items-center rounded-2xl bg-slate-100 text-slate-400">{t("tiktok.noProductImage")}</div>}
<div className="flex items-start justify-between gap-3"><div><h2 className="font-black">{product.title}</h2><p className="text-sm text-slate-500">{product.shopName || t("tiktok.storeUnknown")} · {product.region}</p></div><span className="rounded-full bg-fuchsia-50 px-3 py-1 text-sm font-black text-fuchsia-700">{product.score}</span></div>
<div className="mt-4 grid grid-cols-3 gap-2 text-center text-xs">
<div className="rounded-xl bg-slate-50 p-2"><b>{product.price ? `${product.price} ${product.currency || ""}` : "—"}</b><br />{t("tiktok.price")}</div>
<div className="rounded-xl bg-slate-50 p-2"><b>{(product.soldCount || 0).toLocaleString("tr-TR")}</b><br />{t("tiktok.sales")}</div>
<div className="rounded-xl bg-slate-50 p-2"><b>{product.rating || "—"}</b><br />{t("tiktok.score")}</div>
</div>
{product.productUrl && <a href={product.productUrl} target="_blank" rel="noreferrer" className="mt-4 inline-block text-sm font-bold text-violet-700">{t("tiktok.openOnTiktok")}</a>}
</Card>)}{!ranked.length && <Card className="text-center text-slate-500 md:col-span-2 xl:col-span-3">{t("tiktok.searchFirst")}</Card>}</div>
</div>;
}
+2 -14
View File
@@ -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<string, number>();
@@ -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 <div className="space-y-3">{rows.slice(0, 8).map(([label, value]) => <div key={label}><div className="mb-1 flex justify-between text-sm"><b>{label}</b><span className="text-slate-500">{value}</span></div><div className="h-2 overflow-hidden rounded-full bg-slate-100"><div className="h-full rounded-full bg-violet-600" style={{ width: `${Math.max(4, (value / Math.max(1, total)) * 100)}%` }} /></div></div>)}</div>;
}
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 (
<div>
<div className="mb-6"><h1 className="text-3xl font-black">Trends</h1><p className="mt-1 text-slate-500">Reklam formatı, niche, ülke ve mağaza büyüme sinyallerini canlı veriden karşılaştır.</p></div>
<div className="mb-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4"><Card><div className="text-sm text-slate-500">İzlenen reklam</div><div className="mt-1 text-3xl font-black">{ads.length}</div></Card><Card><div className="text-sm text-slate-500">Aktif reklam</div><div className="mt-1 text-3xl font-black text-emerald-600">{active.length}</div></Card><Card><div className="text-sm text-slate-500">30+ gün yaşayan</div><div className="mt-1 text-3xl font-black">{longRunners.length}</div></Card><Card><div className="text-sm text-slate-500">Son 14 gün</div><div className="mt-1 text-3xl font-black text-violet-700">{newAds.length}</div></Card></div>
<div className="grid gap-5 lg:grid-cols-3"><Card><h2 className="mb-4 text-lg font-black">Niche dağılımı</h2><Bars rows={niches} total={ads.length} /></Card><Card><h2 className="mb-4 text-lg font-black">Medya formatı</h2><Bars rows={media} total={ads.length} /></Card><Card><h2 className="mb-4 text-lg font-black">Ülke sinyali</h2><Bars rows={countries} total={ads.reduce((sum, ad) => sum + ad.countries.length, 0)} /></Card></div>
<Card className="mt-5"><div className="mb-4 flex items-center justify-between"><div><h2 className="text-lg font-black">Hızlı büyüyen mağazalar</h2><p className="text-sm text-slate-500">Aylık ziyaret büyümesine göre</p></div></div><div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">{stores.slice(0, 8).map((store) => <div key={store.name} className="rounded-2xl border border-slate-100 p-4"><div className="font-black">{store.name}</div><div className="text-sm text-slate-500">{store.niche || "Niche yok"} · {store.country || "—"}</div><div className="mt-3 text-2xl font-black text-emerald-600">+{store.monthlyVisitGrowth || 0}%</div></div>)}</div></Card>
<Card className="mt-5"><h2 className="text-lg font-black">14 günlük geçmiş</h2><p className="mb-4 text-sm text-slate-500">Günlük otomasyonla kaydedilen reklam ve TikTok Shop sinyalleri</p><div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">{history.map((snapshot) => { const metrics = snapshot.metrics as any; return <div key={snapshot.id} className="rounded-2xl bg-slate-50 p-3"><b>{snapshot.date.toLocaleDateString("tr-TR")}</b><p className="mt-1 text-sm text-slate-600">{metrics.ads?.active || 0} aktif reklam</p><p className="text-sm text-slate-600">{metrics.tiktok?.total || 0} TikTok ürün</p></div>; })}{!history.length && <p className="text-sm text-slate-500">İlk günlük snapshot henüz oluşmadı.</p>}</div></Card>
</div>
);
return <TrendsClient ads={ads} active={active} longRunners={longRunners} newAds={newAds} niches={niches} media={media} countries={countries} stores={stores} history={history} />;
}
@@ -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 <div className="space-y-3">{rows.slice(0, 8).map(([label, value]) => <div key={label}><div className="mb-1 flex justify-between text-sm"><b>{label}</b><span className="text-slate-500">{value}</span></div><div className="h-2 overflow-hidden rounded-full bg-slate-100"><div className="h-full rounded-full bg-violet-600" style={{ width: `${Math.max(4, (value / Math.max(1, total)) * 100)}%` }} /></div></div>)}</div>;
}
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 (
<div>
<div className="mb-6"><h1 className="text-3xl font-black">{t("trends.title")}</h1><p className="mt-1 text-slate-500">{t("trends.description")}</p></div>
<div className="mb-5 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<Card><div className="text-sm text-slate-500">{t("trends.totalAds")}</div><div className="mt-1 text-3xl font-black">{ads.length}</div></Card>
<Card><div className="text-sm text-slate-500">{t("trends.activeAds")}</div><div className="mt-1 text-3xl font-black text-emerald-600">{active.length}</div></Card>
<Card><div className="text-sm text-slate-500">{t("trends.longRunners")}</div><div className="mt-1 text-3xl font-black">{longRunners.length}</div></Card>
<Card><div className="text-sm text-slate-500">{t("trends.newAds")}</div><div className="mt-1 text-3xl font-black text-violet-700">{newAds.length}</div></Card>
</div>
<div className="grid gap-5 lg:grid-cols-3">
<Card><h2 className="mb-4 text-lg font-black">{t("trends.nicheDistribution")}</h2><Bars rows={niches} total={ads.length} /></Card>
<Card><h2 className="mb-4 text-lg font-black">{t("trends.mediaFormat")}</h2><Bars rows={media} total={ads.length} /></Card>
<Card><h2 className="mb-4 text-lg font-black">{t("trends.countrySignal")}</h2><Bars rows={countries} total={ads.reduce((sum, ad) => sum + ad.countries.length, 0)} /></Card>
</div>
<Card className="mt-5">
<div className="mb-4 flex items-center justify-between">
<div><h2 className="text-lg font-black">{t("trends.fastGrowingStores")}</h2><p className="text-sm text-slate-500">{t("trends.byMonthlyGrowth")}</p></div>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
{stores.slice(0, 8).map((store) => (
<div key={store.name} className="rounded-2xl border border-slate-100 p-4">
<div className="font-black">{store.name}</div>
<div className="text-sm text-slate-500">{store.niche || t("trends.noNiche")} · {store.country || "—"}</div>
<div className="mt-3 text-2xl font-black text-emerald-600">+{store.monthlyVisitGrowth || 0}%</div>
</div>
))}
</div>
</Card>
<Card className="mt-5">
<h2 className="text-lg font-black">{t("trends.historyTitle")}</h2>
<p className="mb-4 text-sm text-slate-500">{t("trends.historyDescription")}</p>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
{history.map((snapshot) => {
const metrics = snapshot.metrics as any;
return (
<div key={snapshot.id} className="rounded-2xl bg-slate-50 p-3">
<b>{snapshot.date.toLocaleDateString("tr-TR")}</b>
<p className="mt-1 text-sm text-slate-600">{metrics.ads?.active || 0} {t("trends.activeAds")}</p>
<p className="text-sm text-slate-600">{metrics.tiktok?.total || 0} {t("trends.tiktokProducts")}</p>
</div>
);
})}
{!history.length && <p className="text-sm text-slate-500">{t("trends.noSnapshotYet")}</p>}
</div>
</Card>
</div>
);
}
+7 -1
View File
@@ -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 (
<html lang="tr">
<body>{children}</body>
<body>
<div className="fixed top-4 right-4 z-50">
<LangSwitch />
</div>
{children}
</body>
</html>
);
}
+33 -31
View File
@@ -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() {
<main className="grid min-h-screen place-items-center bg-slate-50 px-4">
<form onSubmit={submit} className="w-full max-w-md rounded-3xl bg-white p-8 shadow-soft">
<a href="/" className="mb-7 inline-flex"><BrandLogo /></a>
<h1 className="text-3xl font-black">Giriş yap</h1>
<p className="mt-2 text-sm text-slate-500">Hesabınızla veya Google üzerinden güvenli giriş yapın.</p>
<label className="mt-6 block text-sm font-bold">E-posta</label>
<input
type="email"
placeholder="E-posta adresiniz"
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={email}
onChange={(e) => setEmail(e.target.value)}
<h1 className="text-3xl font-black">{t("auth.login.title")}</h1>
<p className="mt-2 text-sm text-slate-500">{t("auth.login.subtitle")}</p>
<label className="mt-6 block text-sm font-bold">{t("auth.email")}</label>
<input
type="email"
placeholder={t("auth.emailPlaceholder")}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<label className="mt-4 block text-sm font-bold">Şifre</label>
<input
type="password"
placeholder="Şifreniz"
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={password}
onChange={(e) => setPassword(e.target.value)}
<label className="mt-4 block text-sm font-bold">{t("auth.password")}</label>
<input
type="password"
placeholder={t("auth.passwordPlaceholder")}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <div className="mt-4 rounded-xl bg-red-50 p-3 text-sm text-red-700">{error}</div>}
<a href="/api/auth/google/start" className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 font-semibold text-slate-700 transition hover:bg-slate-50">
<GoogleIcon />
Google ile devam et
{t("auth.googleContinue")}
</a>
<div className="mt-4 flex items-center gap-3">
<div className="h-px flex-1 bg-slate-200" />
<span className="text-xs font-medium text-slate-400">veya</span>
<span className="text-xs font-medium text-slate-400">{t("auth.or")}</span>
<div className="h-px flex-1 bg-slate-200" />
</div>
<button type="submit" className="mt-4 w-full rounded-xl bg-violet-700 px-4 py-3 font-bold text-white hover:bg-violet-800 transition">
Giriş yap
{t("auth.login.title")}
</button>
<div className="mt-6 text-center text-sm">
<span className="text-slate-500">Hesabınız yok mu? </span>
<span className="text-slate-500">{t("auth.noAccount")} </span>
<a href="/register" className="font-semibold text-violet-700 hover:underline">
Hesap oluştur
{t("auth.createAccount")}
</a>
</div>
</form>
+28 -23
View File
@@ -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 (
<main className="min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,#ddd6fe,transparent_35%),#f8fafc]">
<script
@@ -12,31 +17,31 @@ export default function LandingPage() {
<nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
<BrandLogo />
<div className="flex gap-3">
<LinkButton href="/login" className="bg-white text-slate-900 hover:bg-slate-100">Giriş</LinkButton>
<LinkButton href="/pricing">Planlar</LinkButton>
<LinkButton href="/login" className="bg-white text-slate-900 hover:bg-slate-100">{t("nav.login")}</LinkButton>
<LinkButton href="/pricing">{t("nav.pricing")}</LinkButton>
</div>
</nav>
<section className="mx-auto grid max-w-7xl gap-10 px-6 py-20 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
<div>
<div className="mb-5 inline-flex rounded-full border border-violet-200 bg-white/70 px-4 py-2 text-sm font-semibold text-violet-800">
Meta Ads + TikTok Shop + AI Trend Intelligence
{t("landing.hero.badge")}
</div>
<h1 className="max-w-4xl text-5xl font-black leading-tight tracking-tight text-slate-950 md:text-7xl">
Kazanan reklamları ve mağazaları dakikalar içinde keşfet.
{t("landing.hero.title")}
</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600">
AdSeeQ, reklam ve e-ticaret araştırmalarını tek panelde birleştiren reklam zekâsı platformudur. Meta Ads Library reklamlarını araştırın, TikTok Shop ürünlerini keşfedin, Magic AI ile trend sinyallerini analiz edin ve rakip markaları takip edin.
{t("landing.hero.description")}
</p>
<div className="mt-8 flex flex-wrap gap-3">
<LinkButton href="/register" className="px-6 py-3">Ücretsiz başla</LinkButton>
<LinkButton href="/dashboard/ads" className="bg-slate-950 px-6 py-3 hover:bg-slate-800">Panele git</LinkButton>
<LinkButton href="/register" className="px-6 py-3">{t("landing.hero.cta.primary")}</LinkButton>
<LinkButton href="/dashboard/ads" className="bg-slate-950 px-6 py-3 hover:bg-slate-800">{t("landing.hero.cta.secondary")}</LinkButton>
</div>
</div>
<div className="glass rounded-[2rem] p-4 shadow-2xl">
<div className="rounded-[1.5rem] bg-slate-950 p-5 text-white">
<div className="mb-4 flex items-center justify-between">
<span className="text-sm text-slate-400">Live winners</span>
<span className="rounded-full bg-emerald-400/20 px-3 py-1 text-xs text-emerald-300">Rising</span>
<span className="rounded-full bg-emerald-400/20 px-3 py-1 text-xs text-emerald-300">{t("trends.rising")}</span>
</div>
{["Smart Dog Collar", "LED Face Sculptor", "Greens Energy Blend"].map((item, i) => (
<div key={item} className="mb-3 rounded-2xl bg-white/10 p-4">
@@ -44,7 +49,7 @@ export default function LandingPage() {
<div className="mt-2 h-2 rounded-full bg-white/10">
<div className="h-2 rounded-full bg-violet-400" style={{ width: `${85 - i * 12}%` }} />
</div>
<div className="mt-2 text-xs text-slate-400">Top %{4 + i * 6} · {27 + i * 9} days running</div>
<div className="mt-2 text-xs text-slate-400">Top %{4 + i * 6} · {27 + i * 9} {t("trends.days")}</div>
</div>
))}
</div>
@@ -52,22 +57,22 @@ export default function LandingPage() {
</section>
<section className="mx-auto max-w-7xl px-6 py-16" aria-labelledby="features-heading">
<div className="max-w-3xl">
<p className="text-sm font-black uppercase tracking-[0.2em] text-violet-700">Tek panel, altı araştırma aracı</p>
<p className="text-sm font-black uppercase tracking-[0.2em] text-violet-700">{t("landing.features.subtitle")}</p>
<h2 id="features-heading" className="mt-3 text-3xl font-black tracking-tight text-slate-950 md:text-4xl">
Reklamdan trende, araştırma akışınız tek yerde
{t("landing.features.title")}
</h2>
<p className="mt-4 leading-7 text-slate-600">
Kreatifleri bulun, sinyalleri karşılaştırın ve incelemek istediğiniz reklam, mağaza ve markaları kaydedin.
{t("landing.features.description")}
</p>
</div>
<div className="mt-9 grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{[
["Meta reklam araştırması", "Anahtar kelime ve ülkeye göre medya içeren reklam kreatiflerini araştırın."],
["TikTok Shop keşfi", "Ürünleri ve mağaza sinyallerini aynı araştırma akışında inceleyin."],
["Magic AI", "Kreatif açıları, hedef kitle fikirleri ve test edilebilir yaklaşımlar üretin."],
["Trends", "Yükselen reklam ve ürün sinyallerini düzenli bir görünümde takip edin."],
["Brand Tracker", "Rakip markaları kaydedin ve yeni hareketlerini tek listeden izleyin."],
["Store Tracker", "Mağazaları takip edin, benzer mağazaları ve ürün sinyallerini keşfedin."]
[t("feature.meta.title"), t("feature.meta.description")],
[t("feature.tiktok.title"), t("feature.tiktok.description")],
[t("feature.ai.title"), t("feature.ai.description")],
[t("feature.trends.title"), t("feature.trends.description")],
[t("feature.brand.title"), t("feature.brand.description")],
[t("feature.store.title"), t("feature.store.description")]
].map(([title, description]) => (
<article key={title} className="rounded-3xl border border-slate-200 bg-white/80 p-6 shadow-sm">
<h3 className="text-lg font-black text-slate-950">{title}</h3>
@@ -78,8 +83,8 @@ export default function LandingPage() {
</section>
<section className="border-y border-slate-200 bg-white/70" aria-labelledby="faq-heading">
<div className="mx-auto max-w-4xl px-6 py-16">
<p className="text-sm font-black uppercase tracking-[0.2em] text-violet-700">Sık sorulan sorular</p>
<h2 id="faq-heading" className="mt-3 text-3xl font-black tracking-tight text-slate-950">AdSeeQ hakkında</h2>
<p className="text-sm font-black uppercase tracking-[0.2em] text-violet-700">{t("faq.subtitle")}</p>
<h2 id="faq-heading" className="mt-3 text-3xl font-black tracking-tight text-slate-950">{t("faq.title")}</h2>
<div className="mt-8 space-y-4">
{homeFaq.map(({ question, answer }) => (
<details key={question} className="group rounded-2xl border border-slate-200 bg-white p-5">
@@ -93,9 +98,9 @@ export default function LandingPage() {
<footer className="mx-auto flex max-w-7xl flex-col gap-4 px-6 py-10 text-sm text-slate-500 sm:flex-row sm:items-center sm:justify-between">
<BrandLogo markClassName="h-8 w-8" />
<div className="flex gap-5">
<a href="/pricing" className="hover:text-slate-950">Fiyatlandırma</a>
<a href="/register" className="hover:text-slate-950">Ücretsiz başla</a>
<a href="/login" className="hover:text-slate-950">Giriş</a>
<a href="/pricing" className="hover:text-slate-950">{t("footer.pricing")}</a>
<a href="/register" className="hover:text-slate-950">{t("footer.startFree")}</a>
<a href="/login" className="hover:text-slate-950">{t("footer.signIn")}</a>
</div>
</footer>
</main>
+2 -30
View File
@@ -1,8 +1,5 @@
import { prisma } from "@/lib/db";
import { LinkButton } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { CheckoutButton } from "@/components/checkout-button";
import { BrandLogo } from "@/components/brand-logo";
import { PricingClient } from "./pricing-client";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -25,30 +22,5 @@ export default async function PricingPage() {
{ code: "PREMIUM", name: "Premium", monthlyPriceEur: 212, features: { adsSearchDaily: 2000, trackedStores: 500 } }
];
const rows = plans.length ? plans : fallback;
return (
<main className="min-h-screen bg-slate-50 px-6 py-12">
<div className="mx-auto max-w-6xl">
<a href="/" className="mb-10 inline-flex"><BrandLogo /></a>
<div className="mb-10 text-center">
<h1 className="text-4xl font-black">Fiyatlandırma</h1>
<p className="mt-3 text-slate-600">Kota tabanlı freemium model: arama, takip ve API kredileri planlara göre açılır.</p>
</div>
<div className="grid gap-5 md:grid-cols-4">
{rows.map((plan: any) => (
<Card key={plan.code} className={plan.code === "STANDARD" ? "ring-2 ring-violet-600" : ""}>
<div className="text-sm font-bold text-violet-700">{plan.code}</div>
<h2 className="mt-2 text-2xl font-black">{plan.name}</h2>
<div className="mt-4 text-4xl font-black">€{plan.monthlyPriceEur}<span className="text-sm font-medium text-slate-500">/ay</span></div>
<ul className="mt-6 space-y-2 text-sm text-slate-600">
<li>Ads arama/gün: {String((plan.features as any).adsSearchDaily ?? "Sınırsız")}</li>
<li>Store tracker: {String((plan.features as any).trackedStores ?? "Sınırsız")}</li>
<li>Saved ads: {String((plan.features as any).savedAds ?? "Sınırsız")}</li>
</ul>
{plan.code === "FREE" ? <LinkButton href="/register" className="mt-6 w-full">Ücretsiz başla</LinkButton> : <CheckoutButton planCode={plan.code} />}
</Card>
))}
</div>
</div>
</main>
);
return <PricingClient plans={rows} />;
}
+36
View File
@@ -0,0 +1,36 @@
"use client";
import { LinkButton } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { CheckoutButton } from "@/components/checkout-button";
import { BrandLogo } from "@/components/brand-logo";
import { useT } from "@/lib/i18n";
export function PricingClient({ plans }: { plans: any[] }) {
const t = useT();
return (
<main className="min-h-screen bg-slate-50 px-6 py-12">
<div className="mx-auto max-w-6xl">
<a href="/" className="mb-10 inline-flex"><BrandLogo /></a>
<div className="mb-10 text-center">
<h1 className="text-4xl font-black">{t("pricing.title")}</h1>
<p className="mt-3 text-slate-600">{t("pricing.description")}</p>
</div>
<div className="grid gap-5 md:grid-cols-4">
{plans.map((plan: any) => (
<Card key={plan.code} className={plan.code === "STANDARD" ? "ring-2 ring-violet-600" : ""}>
<div className="text-sm font-bold text-violet-700">{plan.code}</div>
<h2 className="mt-2 text-2xl font-black">{plan.name}</h2>
<div className="mt-4 text-4xl font-black">€{plan.monthlyPriceEur}<span className="text-sm font-medium text-slate-500">{t("pricing.perMonth")}</span></div>
<ul className="mt-6 space-y-2 text-sm text-slate-600">
<li>{t("pricing.adsPerDay")}: {String((plan.features as any).adsSearchDaily ?? t("pricing.unlimited"))}</li>
<li>{t("pricing.storeTracker")}: {String((plan.features as any).trackedStores ?? t("pricing.unlimited"))}</li>
<li>{t("pricing.savedAds")}: {String((plan.features as any).savedAds ?? t("pricing.unlimited"))}</li>
</ul>
{plan.code === "FREE" ? <LinkButton href="/register" className="mt-6 w-full">{t("pricing.startFree")}</LinkButton> : <CheckoutButton planCode={plan.code} />}
</Card>
))}
</div>
</div>
</main>
);
}
+34 -32
View File
@@ -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 RegisterPage() {
const t = useT();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [name, setName] = useState("");
@@ -25,7 +27,7 @@ export default function RegisterPage() {
setError("");
const res = await fetch("/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ email, password, name }) });
if (!res.ok) {
setError("Kayıt başarısız. E-posta kullanılıyor olabilir veya seed çalışmamış olabilir.");
setError(t("auth.registerError"));
return;
}
location.href = "/dashboard/ads";
@@ -35,58 +37,58 @@ export default function RegisterPage() {
<main className="grid min-h-screen place-items-center bg-slate-50 px-4">
<form onSubmit={submit} className="w-full max-w-md rounded-3xl bg-white p-8 shadow-soft">
<a href="/" className="mb-7 inline-flex"><BrandLogo /></a>
<h1 className="text-3xl font-black">Ücretsiz başla</h1>
<label className="mt-6 block text-sm font-bold">Ad</label>
<input
placeholder="Adınız"
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={name}
onChange={(e) => setName(e.target.value)}
<h1 className="text-3xl font-black">{t("landing.hero.cta.primary")}</h1>
<label className="mt-6 block text-sm font-bold">{t("auth.name")}</label>
<input
placeholder={t("auth.namePlaceholder")}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<label className="mt-4 block text-sm font-bold">E-posta</label>
<input
<label className="mt-4 block text-sm font-bold">{t("auth.email")}</label>
<input
type="email"
placeholder="E-posta adresiniz"
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={t("auth.emailPlaceholder")}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<label className="mt-4 block text-sm font-bold">Şifre</label>
<input
type="password"
placeholder="En az 10 karakterli şifreniz"
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={password}
onChange={(e) => setPassword(e.target.value)}
<label className="mt-4 block text-sm font-bold">{t("auth.password")}</label>
<input
type="password"
placeholder={t("auth.passwordPlaceholderLong")}
className="mt-2 w-full rounded-xl border border-slate-200 px-4 py-3 focus:outline-none focus:ring-2 focus:ring-violet-600"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <div className="mt-4 rounded-xl bg-red-50 p-3 text-sm text-red-700">{error}</div>}
<a href="/api/auth/google/start" className="mt-6 flex w-full items-center justify-center gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 font-semibold text-slate-700 transition hover:bg-slate-50">
<GoogleIcon />
Google ile devam et
{t("auth.googleContinue")}
</a>
<div className="mt-4 flex items-center gap-3">
<div className="h-px flex-1 bg-slate-200" />
<span className="text-xs font-medium text-slate-400">veya</span>
<span className="text-xs font-medium text-slate-400">{t("auth.or")}</span>
<div className="h-px flex-1 bg-slate-200" />
</div>
<button className="mt-4 w-full rounded-xl bg-violet-700 px-4 py-3 font-bold text-white hover:bg-violet-800 transition">
Hesap oluştur
{t("auth.createAccount")}
</button>
<div className="mt-6 text-center text-sm">
<span className="text-slate-500">Zaten hesabınız var mı? </span>
<span className="text-slate-500">{t("auth.haveAccount")} </span>
<a href="/login" className="font-semibold text-violet-700 hover:underline">
Giriş yap
{t("auth.login.title")}
</a>
</div>
</form>