forked from hikmet4434/winninghunter
Initial WinningHunter MVP
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireAdmin } from "@/lib/auth/current-user";
|
||||
|
||||
export async function POST() {
|
||||
await requireAdmin();
|
||||
return NextResponse.json({ ok: true, message: "Demo seed komut satırından çalışır: npm run db:seed" });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { maskAdForPlan } from "@/lib/locked-response";
|
||||
import { planFromUser } from "@/lib/plans";
|
||||
|
||||
export async function GET(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const ad = await prisma.ad.findUnique({
|
||||
where: { id: params.id },
|
||||
include: { brandPage: true, creatives: true, aiTags: true, countryStats: true }
|
||||
});
|
||||
if (!ad) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 });
|
||||
const plan = planFromUser(user as any);
|
||||
return NextResponse.json({ data: maskAdForPlan(ad as any, plan?.code) });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function POST(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const ad = await prisma.ad.findUnique({ where: { id: params.id } });
|
||||
if (!ad) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 });
|
||||
await prisma.savedAd.upsert({
|
||||
where: { userId_adId: { userId: user.id, adId: params.id } },
|
||||
update: {},
|
||||
create: { userId: user.id, adId: params.id }
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
await prisma.savedAd.deleteMany({ where: { userId: user.id, adId: params.id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { checkAndConsumeQuota } from "@/lib/quota";
|
||||
import { maskAdForPlan } from "@/lib/locked-response";
|
||||
import { planFromUser } from "@/lib/plans";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
|
||||
const plan = planFromUser(user as any);
|
||||
const quota = await checkAndConsumeQuota({ userId: user.id, plan, metric: "ads_search_daily" });
|
||||
if (!quota.allowed) return NextResponse.json({ error: "QUOTA_EXCEEDED", usage: quota, upgradeRequired: true }, { status: 403 });
|
||||
|
||||
const url = new URL(req.url);
|
||||
const q = url.searchParams.get("q")?.trim();
|
||||
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
|
||||
const limit = Math.min(60, Math.max(1, Number(url.searchParams.get("limit") || 24)));
|
||||
const status = url.searchParams.get("status");
|
||||
const niche = url.searchParams.get("niche");
|
||||
const mediaType = url.searchParams.get("mediaType");
|
||||
const country = url.searchParams.get("country");
|
||||
const sort = url.searchParams.get("sort") || "lastSeen_desc";
|
||||
|
||||
const where: Prisma.AdWhereInput = {};
|
||||
if (status) where.status = status as any;
|
||||
if (niche) where.niche = { contains: niche, mode: "insensitive" };
|
||||
if (mediaType) where.mediaType = mediaType as any;
|
||||
if (country) where.countries = { has: country };
|
||||
if (q) {
|
||||
where.OR = [
|
||||
{ primaryText: { contains: q, mode: "insensitive" } },
|
||||
{ headline: { contains: q, mode: "insensitive" } },
|
||||
{ brandPage: { name: { contains: q, mode: "insensitive" } } }
|
||||
];
|
||||
}
|
||||
|
||||
const orderBy: Prisma.AdOrderByWithRelationInput =
|
||||
sort === "rank_asc" ? { rankPercentile: "asc" } : sort === "days_desc" ? { daysRunning: "desc" } : { lastSeenAt: "desc" };
|
||||
|
||||
const [total, rows, saved] = await Promise.all([
|
||||
prisma.ad.count({ where }),
|
||||
prisma.ad.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
include: { brandPage: true, creatives: { take: 1 } }
|
||||
}),
|
||||
prisma.savedAd.findMany({ where: { userId: user.id }, select: { adId: true } })
|
||||
]);
|
||||
|
||||
const savedIds = new Set(saved.map((s) => s.adId));
|
||||
const data = rows.map((ad) =>
|
||||
maskAdForPlan(
|
||||
{
|
||||
id: ad.id,
|
||||
source: ad.source,
|
||||
status: ad.status,
|
||||
mediaType: ad.mediaType,
|
||||
adScore: ad.adScore,
|
||||
primaryText: ad.primaryText,
|
||||
headline: ad.headline,
|
||||
ctaText: ad.ctaText,
|
||||
landingUrl: ad.landingUrl,
|
||||
productUrl: ad.productUrl,
|
||||
language: ad.language,
|
||||
countries: ad.countries,
|
||||
niche: ad.niche,
|
||||
daysRunning: ad.daysRunning,
|
||||
estimatedReachMin: ad.estimatedReachMin,
|
||||
estimatedReachMax: ad.estimatedReachMax,
|
||||
estimatedSpendMin: ad.estimatedSpendMin,
|
||||
estimatedSpendMax: ad.estimatedSpendMax,
|
||||
rankPercentile: ad.rankPercentile,
|
||||
brand: ad.brandPage && { id: ad.brandPage.id, name: ad.brandPage.name, logoUrl: ad.brandPage.logoUrl },
|
||||
thumbnailUrl: ad.creatives[0]?.thumbnailUrl || ad.creatives[0]?.url,
|
||||
isSaved: savedIds.has(ad.id)
|
||||
},
|
||||
plan?.code
|
||||
)
|
||||
);
|
||||
|
||||
return NextResponse.json({ data, pagination: { page, limit, total }, usage: quota });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
|
||||
const schema = z.object({ email: z.string().email(), password: z.string().min(1) });
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = schema.parse(await req.json());
|
||||
const user = await prisma.user.findUnique({ where: { email: body.email } });
|
||||
if (!user?.passwordHash) return NextResponse.json({ error: "INVALID_CREDENTIALS" }, { status: 401 });
|
||||
const ok = await bcrypt.compare(body.password, user.passwordHash);
|
||||
if (!ok) return NextResponse.json({ error: "INVALID_CREDENTIALS" }, { status: 401 });
|
||||
await prisma.user.update({ where: { id: user.id }, data: { lastLoginAt: new Date() } });
|
||||
await createSession(user.id);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { destroySession } from "@/lib/auth/session";
|
||||
|
||||
export async function POST() {
|
||||
await destroySession();
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ user: null });
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
plan: user.subscription?.plan.code
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { createSession } from "@/lib/auth/session";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
name: z.string().optional()
|
||||
});
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const body = schema.parse(await req.json());
|
||||
const free = await prisma.plan.findUnique({ where: { code: "FREE" } });
|
||||
if (!free) return NextResponse.json({ error: "PLAN_NOT_SEEDED" }, { status: 500 });
|
||||
|
||||
const exists = await prisma.user.findUnique({ where: { email: body.email } });
|
||||
if (exists) return NextResponse.json({ error: "EMAIL_EXISTS" }, { status: 409 });
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email: body.email,
|
||||
name: body.name || body.email.split("@")[0],
|
||||
passwordHash: await bcrypt.hash(body.password, 10),
|
||||
subscription: { create: { planId: free.id, status: "ACTIVE" } }
|
||||
}
|
||||
});
|
||||
await createSession(user.id);
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export async function GET() {
|
||||
const plans = await prisma.plan.findMany({ where: { isActive: true }, orderBy: { sortOrder: "asc" } });
|
||||
return NextResponse.json({ data: plans });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const body = await req.json();
|
||||
const saved = await prisma.savedAd.updateMany({ where: { id: params.id, userId: user.id }, data: { folderId: body.folderId || null, note: body.note } });
|
||||
return NextResponse.json({ data: saved });
|
||||
}
|
||||
|
||||
export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
await prisma.savedAd.deleteMany({ where: { id: params.id, userId: user.id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const folderId = new URL(req.url).searchParams.get("folderId");
|
||||
const data = await prisma.savedAd.findMany({
|
||||
where: { userId: user.id, ...(folderId ? { folderId } : {}) },
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: { folder: true, ad: { include: { brandPage: true, creatives: { take: 1 } } } }
|
||||
});
|
||||
return NextResponse.json({ data });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const body = await req.json();
|
||||
const saved = await prisma.savedAd.upsert({
|
||||
where: { userId_adId: { userId: user.id, adId: body.adId } },
|
||||
update: { folderId: body.folderId || null, note: body.note },
|
||||
create: { userId: user.id, adId: body.adId, folderId: body.folderId || null, note: body.note }
|
||||
});
|
||||
return NextResponse.json({ data: saved });
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function PATCH(req: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const body = await req.json();
|
||||
await prisma.savedFolder.updateMany({ where: { id: params.id, userId: user.id }, data: { name: body.name, color: body.color } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
await prisma.savedFolder.deleteMany({ where: { id: params.id, userId: user.id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
return NextResponse.json({ data: await prisma.savedFolder.findMany({ where: { userId: user.id }, orderBy: { sortOrder: "asc" } }) });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const body = await req.json();
|
||||
const folder = await prisma.savedFolder.create({ data: { userId: user.id, name: body.name, color: body.color } });
|
||||
return NextResponse.json({ data: folder });
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const store = await prisma.store.findUnique({ where: { id: params.id }, include: { products: true, snapshots: { orderBy: { date: "asc" } }, pixels: true, apps: true, brandPages: { include: { ads: { include: { creatives: { take: 1 } }, take: 12 } } } } });
|
||||
if (!store) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 });
|
||||
return NextResponse.json({ data: store });
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const store = await prisma.store.findUnique({ where: { id: params.id } });
|
||||
if (!store) return NextResponse.json({ error: "NOT_FOUND" }, { status: 404 });
|
||||
const data = await prisma.store.findMany({ where: { id: { not: params.id }, niche: store.niche }, take: 20, orderBy: { monthlyVisits: "desc" } });
|
||||
return NextResponse.json({ data });
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
|
||||
const url = new URL(req.url);
|
||||
const q = url.searchParams.get("q")?.trim();
|
||||
const niche = url.searchParams.get("niche");
|
||||
const country = url.searchParams.get("country");
|
||||
const sort = url.searchParams.get("sort") || "revenue_desc";
|
||||
const page = Math.max(1, Number(url.searchParams.get("page") || 1));
|
||||
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit") || 25)));
|
||||
|
||||
const where: Prisma.StoreWhereInput = {};
|
||||
if (niche) where.niche = { contains: niche, mode: "insensitive" };
|
||||
if (country) where.country = country;
|
||||
if (q) where.OR = [{ name: { contains: q, mode: "insensitive" } }, { domain: { contains: q, mode: "insensitive" } }, { niche: { contains: q, mode: "insensitive" } }];
|
||||
|
||||
const orderBy: Prisma.StoreOrderByWithRelationInput = sort === "traffic_desc" ? { monthlyVisits: "desc" } : sort === "growth_desc" ? { monthlyVisitGrowth: "desc" } : { estRevenue30dMax: "desc" };
|
||||
|
||||
const [total, rows] = await Promise.all([
|
||||
prisma.store.count({ where }),
|
||||
prisma.store.findMany({ where, orderBy, skip: (page - 1) * limit, take: limit, include: { products: { where: { isBestSeller: true }, take: 3 }, brandPages: { include: { _count: { select: { ads: true } } }, take: 1 } } })
|
||||
]);
|
||||
return NextResponse.json({ data: rows, pagination: { page, limit, total } });
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
|
||||
export async function DELETE(_: Request, context: { params: Promise<{ id: string }> }) {
|
||||
const params = await context.params;
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
await prisma.trackedStore.deleteMany({ where: { id: params.id, userId: user.id } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { getFeatureLimit, planFromUser } from "@/lib/plans";
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const data = await prisma.trackedStore.findMany({ where: { userId: user.id }, include: { store: { include: { products: { where: { isBestSeller: true }, take: 3 } } } }, orderBy: { createdAt: "desc" } });
|
||||
return NextResponse.json({ data });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
const plan = planFromUser(user as any);
|
||||
const limit = getFeatureLimit(plan as any, "tracked_stores");
|
||||
const count = await prisma.trackedStore.count({ where: { userId: user.id } });
|
||||
if (limit !== null && count >= limit) return NextResponse.json({ error: "TRACKED_STORE_LIMIT_EXCEEDED", upgradeRequired: true }, { status: 403 });
|
||||
const body = await req.json();
|
||||
const domain = String(body.domain || "").replace(/^https?:\/\//, "").replace(/\/$/, "").toLowerCase();
|
||||
const store = await prisma.store.findUnique({ where: { domain } });
|
||||
if (!store) return NextResponse.json({ error: "STORE_NOT_FOUND" }, { status: 404 });
|
||||
const row = await prisma.trackedStore.upsert({ where: { userId_storeId: { userId: user.id, storeId: store.id } }, update: {}, create: { userId: user.id, storeId: store.id } });
|
||||
return NextResponse.json({ data: row });
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { currentUser } from "@/lib/auth/current-user";
|
||||
import { usageSummary } from "@/lib/quota";
|
||||
|
||||
export async function GET() {
|
||||
const user = await currentUser();
|
||||
if (!user) return NextResponse.json({ error: "UNAUTHORIZED" }, { status: 401 });
|
||||
return NextResponse.json({ data: await usageSummary(user.id) });
|
||||
}
|
||||
Reference in New Issue
Block a user