feat: rebrand WinningHunter as AdSeeQ
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
NEXT_PUBLIC_APP_URL=https://your-domain.com
|
NEXT_PUBLIC_APP_URL=https://adseeq.com
|
||||||
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/winninghunter?schema=public"
|
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/winninghunter?schema=public"
|
||||||
AUTH_COOKIE_NAME=wh_session
|
AUTH_COOKIE_NAME=wh_session
|
||||||
AUTH_SESSION_DAYS=30
|
AUTH_SESSION_DAYS=30
|
||||||
ADMIN_EMAILS="admin@winninghunter.local"
|
ADMIN_EMAILS="admin@winninghunter.local"
|
||||||
|
|
||||||
# Google OAuth (https://console.cloud.google.com/apis/credentials)
|
# Google OAuth (https://console.cloud.google.com/apis/credentials)
|
||||||
# Authorized redirect URI: {NEXT_PUBLIC_APP_URL}/api/auth/google/callback
|
# Authorized redirect URI: https://adseeq.com/api/auth/google/callback
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# WinningHunter MVP
|
# AdSeeQ
|
||||||
|
|
||||||
WinningHunter klonu için çalışan MVP:
|
Meta reklamları, TikTok Shop ürünleri, trendler ve rakip markalar için çalışan reklam zekâsı platformu:
|
||||||
|
|
||||||
- Next.js 16 + React 19
|
- Next.js 16 + React 19
|
||||||
- Prisma + PostgreSQL
|
- Prisma + PostgreSQL
|
||||||
@@ -61,11 +61,11 @@ docker compose up -d --build
|
|||||||
1. Yeni proje oluştur.
|
1. Yeni proje oluştur.
|
||||||
2. Git repo veya ZIP kaynak olarak bu klasörü bağla.
|
2. Git repo veya ZIP kaynak olarak bu klasörü bağla.
|
||||||
3. Build type: Dockerfile.
|
3. Build type: Dockerfile.
|
||||||
4. Domain: `winninghunter.your-domain.com`.
|
4. Domain: `adseeq.com`.
|
||||||
5. Env:
|
5. Env:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
NEXT_PUBLIC_APP_URL=https://winninghunter.your-domain.com
|
NEXT_PUBLIC_APP_URL=https://adseeq.com
|
||||||
DATABASE_URL=postgresql://...
|
DATABASE_URL=postgresql://...
|
||||||
AUTH_COOKIE_NAME=wh_session
|
AUTH_COOKIE_NAME=wh_session
|
||||||
AUTH_SESSION_DAYS=30
|
AUTH_SESSION_DAYS=30
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "winninghunter",
|
"name": "adseeq",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "winninghunter",
|
"name": "adseeq",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.22.0",
|
"@prisma/client": "5.22.0",
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "winninghunter",
|
"name": "adseeq",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ set -eu
|
|||||||
|
|
||||||
app_url="${NEXT_PUBLIC_APP_URL:?NEXT_PUBLIC_APP_URL is required}"
|
app_url="${NEXT_PUBLIC_APP_URL:?NEXT_PUBLIC_APP_URL is required}"
|
||||||
if curl --fail --silent --show-error --max-time 10 "${app_url%/}/api/health" >/dev/null; then
|
if curl --fail --silent --show-error --max-time 10 "${app_url%/}/api/health" >/dev/null; then
|
||||||
echo "WinningHunter healthcheck OK"
|
echo "AdSeeQ healthcheck OK"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${ALERT_WEBHOOK_URL:-}" ]; then
|
if [ -n "${ALERT_WEBHOOK_URL:-}" ]; then
|
||||||
curl --fail --silent --show-error --max-time 10 -X POST -H 'content-type: application/json' --data '{"text":"WinningHunter healthcheck failed"}' "$ALERT_WEBHOOK_URL" >/dev/null || true
|
curl --fail --silent --show-error --max-time 10 -X POST -H 'content-type: application/json' --data '{"text":"AdSeeQ healthcheck failed"}' "$ALERT_WEBHOOK_URL" >/dev/null || true
|
||||||
fi
|
fi
|
||||||
echo "WinningHunter healthcheck FAILED" >&2
|
echo "AdSeeQ healthcheck FAILED" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default async function AdminOverviewPage() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-6 flex flex-wrap items-end justify-between gap-3">
|
<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">WinningHunter Admin</h1><p className="mt-1 text-slate-500">Kullanıcı, gelir, kredi ve operasyon görünümü.</p></div>
|
<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 className="rounded-2xl bg-emerald-50 px-4 py-2 text-sm font-bold text-emerald-700">Sistem aktif</div>
|
||||||
</div>
|
</div>
|
||||||
<AdminNav />
|
<AdminNav />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { requireUser } from "@/lib/auth/current-user";
|
import { requireUser } from "@/lib/auth/current-user";
|
||||||
import { DashboardMobileNav } from "@/components/dashboard-mobile-nav";
|
import { DashboardMobileNav } from "@/components/dashboard-mobile-nav";
|
||||||
|
import { BrandLogo } from "@/components/brand-logo";
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
["Ads", "/dashboard/ads"],
|
["Ads", "/dashboard/ads"],
|
||||||
@@ -20,7 +21,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
|
|||||||
<div className="min-h-screen bg-slate-50">
|
<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">
|
<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">
|
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4">
|
||||||
<Link href="/dashboard/ads" className="text-xl font-black">WinningHunter<span className="text-violet-700">.AI</span></Link>
|
<Link href="/dashboard/ads"><BrandLogo markClassName="h-8 w-8" /></Link>
|
||||||
<DashboardMobileNav isAdmin={user.role === "ADMIN"} />
|
<DashboardMobileNav isAdmin={user.role === "ADMIN"} />
|
||||||
<nav className="hidden gap-1 lg:flex">
|
<nav className="hidden gap-1 lg:flex">
|
||||||
{nav.map(([label, href]) => (
|
{nav.map(([label, href]) => (
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||||
|
<rect width="64" height="64" rx="18" fill="#6d28d9"/>
|
||||||
|
<circle cx="29" cy="29" r="16" fill="none" stroke="#fff" stroke-width="7"/>
|
||||||
|
<path d="M41 41 54 54" fill="none" stroke="#fff" stroke-linecap="round" stroke-width="7"/>
|
||||||
|
<path d="m21 29 5 5 11-11" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 416 B |
+25
-2
@@ -1,9 +1,32 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
|
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "WinningHunter MVP",
|
metadataBase: new URL(appUrl),
|
||||||
description: "Dropshipping reklam ve mağaza istihbaratı MVP"
|
applicationName: "AdSeeQ",
|
||||||
|
title: {
|
||||||
|
default: "AdSeeQ — Reklam ve Trend Zekâsı",
|
||||||
|
template: "%s | AdSeeQ"
|
||||||
|
},
|
||||||
|
description: "Meta reklamlarını, TikTok Shop ürünlerini, yükselen trendleri ve rakip markaları yapay zekâ ile tek panelden keşfedin.",
|
||||||
|
keywords: ["reklam kütüphanesi", "Meta Ads", "TikTok Shop", "trend analizi", "marka takibi", "reklam zekâsı"],
|
||||||
|
alternates: { canonical: "/" },
|
||||||
|
openGraph: {
|
||||||
|
title: "AdSeeQ — Kazanan reklamları ve trendleri keşfedin",
|
||||||
|
description: "Meta Ads, TikTok Shop, Magic AI Trends ve Brand Tracker tek panelde.",
|
||||||
|
url: "/",
|
||||||
|
siteName: "AdSeeQ",
|
||||||
|
locale: "tr_TR",
|
||||||
|
type: "website"
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary",
|
||||||
|
title: "AdSeeQ — Reklam ve Trend Zekâsı",
|
||||||
|
description: "Kazanan reklamları, ürünleri ve rakip marka hareketlerini keşfedin."
|
||||||
|
},
|
||||||
|
robots: { index: true, follow: true }
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { BrandLogo } from "@/components/brand-logo";
|
||||||
|
|
||||||
function GoogleIcon() {
|
function GoogleIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -36,6 +37,7 @@ export default function LoginPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="grid min-h-screen place-items-center bg-slate-50 px-4">
|
<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">
|
<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>
|
<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>
|
<p className="mt-2 text-sm text-slate-500">Hesabınızla veya Google üzerinden güvenli giriş yapın.</p>
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -1,10 +1,11 @@
|
|||||||
import { LinkButton } from "@/components/ui/button";
|
import { LinkButton } from "@/components/ui/button";
|
||||||
|
import { BrandLogo } from "@/components/brand-logo";
|
||||||
|
|
||||||
export default function LandingPage() {
|
export default function LandingPage() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,#ddd6fe,transparent_35%),#f8fafc]">
|
<main className="min-h-screen overflow-hidden bg-[radial-gradient(circle_at_top_left,#ddd6fe,transparent_35%),#f8fafc]">
|
||||||
<nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
|
<nav className="mx-auto flex max-w-7xl items-center justify-between px-6 py-6">
|
||||||
<div className="text-xl font-black tracking-tight">WinningHunter<span className="text-violet-700">.AI</span></div>
|
<BrandLogo />
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<LinkButton href="/login" className="bg-white text-slate-900 hover:bg-slate-100">Giriş</LinkButton>
|
<LinkButton href="/login" className="bg-white text-slate-900 hover:bg-slate-100">Giriş</LinkButton>
|
||||||
<LinkButton href="/pricing">Planlar</LinkButton>
|
<LinkButton href="/pricing">Planlar</LinkButton>
|
||||||
@@ -13,13 +14,13 @@ export default function LandingPage() {
|
|||||||
<section className="mx-auto grid max-w-7xl gap-10 px-6 py-20 lg:grid-cols-[1.05fr_0.95fr] lg:items-center">
|
<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>
|
||||||
<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">
|
<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 + Shopify Store Intelligence MVP
|
Meta Ads + TikTok Shop + AI Trend Intelligence
|
||||||
</div>
|
</div>
|
||||||
<h1 className="max-w-4xl text-5xl font-black leading-tight tracking-tight text-slate-950 md:text-7xl">
|
<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.
|
Kazanan reklamları ve mağazaları dakikalar içinde keşfet.
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600">
|
<p className="mt-6 max-w-2xl text-lg leading-8 text-slate-600">
|
||||||
Dropshipping ve e-ticaret için reklam kütüphanesi, mağaza keşfi, kaydetme, takip ve kota tabanlı üyelik altyapısı tek panelde.
|
Meta reklam kütüphanesi, TikTok Shop ürün keşfi, Magic AI trend analizi ve rakip marka takibi tek panelde.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
<div className="mt-8 flex flex-wrap gap-3">
|
||||||
<LinkButton href="/register" className="px-6 py-3">Ücretsiz başla</LinkButton>
|
<LinkButton href="/register" className="px-6 py-3">Ücretsiz başla</LinkButton>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { prisma } from "@/lib/db";
|
|||||||
import { LinkButton } from "@/components/ui/button";
|
import { LinkButton } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { CheckoutButton } from "@/components/checkout-button";
|
import { CheckoutButton } from "@/components/checkout-button";
|
||||||
|
import { BrandLogo } from "@/components/brand-logo";
|
||||||
|
|
||||||
export default async function PricingPage() {
|
export default async function PricingPage() {
|
||||||
const plans = await prisma.plan.findMany({ orderBy: { sortOrder: "asc" } }).catch(() => []);
|
const plans = await prisma.plan.findMany({ orderBy: { sortOrder: "asc" } }).catch(() => []);
|
||||||
@@ -15,6 +16,7 @@ export default async function PricingPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-slate-50 px-6 py-12">
|
<main className="min-h-screen bg-slate-50 px-6 py-12">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
|
<a href="/" className="mb-10 inline-flex"><BrandLogo /></a>
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<h1 className="text-4xl font-black">Fiyatlandırma</h1>
|
<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>
|
<p className="mt-3 text-slate-600">Kota tabanlı freemium model: arama, takip ve API kredileri planlara göre açılır.</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { BrandLogo } from "@/components/brand-logo";
|
||||||
|
|
||||||
function GoogleIcon() {
|
function GoogleIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -33,6 +34,7 @@ export default function RegisterPage() {
|
|||||||
return (
|
return (
|
||||||
<main className="grid min-h-screen place-items-center bg-slate-50 px-4">
|
<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">
|
<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>
|
<h1 className="text-3xl font-black">Ücretsiz başla</h1>
|
||||||
|
|
||||||
<label className="mt-6 block text-sm font-bold">Ad</label>
|
<label className="mt-6 block text-sm font-bold">Ad</label>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function ApifyEmptySearch({ query, planLimit }: { query: string; planLimi
|
|||||||
<div className="rounded-3xl border border-dashed border-violet-200 bg-violet-50/60 p-8 text-center md:col-span-2 xl:col-span-3">
|
<div className="rounded-3xl border border-dashed border-violet-200 bg-violet-50/60 p-8 text-center md:col-span-2 xl:col-span-3">
|
||||||
<h2 className="text-xl font-black">“{query}” için kayıtlı reklam bulunamadı</h2>
|
<h2 className="text-xl font-black">“{query}” için kayıtlı reklam bulunamadı</h2>
|
||||||
<p className="mx-auto mt-2 max-w-2xl text-sm text-slate-600">
|
<p className="mx-auto mt-2 max-w-2xl text-sm text-slate-600">
|
||||||
Bu arama önce WinningHunter veritabanını kontrol eder. Yeni Meta reklamlarını Apify üzerinden getirip aynı aramaya ekleyebilirsiniz.
|
Bu arama önce AdSeeQ veritabanını kontrol eder. Yeni Meta reklamlarını Apify üzerinden getirip aynı aramaya ekleyebilirsiniz.
|
||||||
</p>
|
</p>
|
||||||
<div className="mx-auto mt-5 flex max-w-sm gap-2">
|
<div className="mx-auto mt-5 flex max-w-sm gap-2">
|
||||||
<label className="sr-only" htmlFor="apify-result-count">Getirilecek reklam adedi</label>
|
<label className="sr-only" htmlFor="apify-result-count">Getirilecek reklam adedi</label>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
type BrandLogoProps = {
|
||||||
|
className?: string;
|
||||||
|
markClassName?: string;
|
||||||
|
showWordmark?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BrandLogo({
|
||||||
|
className = "",
|
||||||
|
markClassName = "h-9 w-9",
|
||||||
|
showWordmark = true
|
||||||
|
}: BrandLogoProps) {
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-2.5 ${className}`} aria-label="AdSeeQ">
|
||||||
|
<span className={`inline-flex shrink-0 items-center justify-center rounded-xl bg-violet-700 text-white shadow-sm ${markClassName}`}>
|
||||||
|
<svg viewBox="0 0 32 32" className="h-[72%] w-[72%]" aria-hidden="true">
|
||||||
|
<circle cx="14.5" cy="14.5" r="8" fill="none" stroke="currentColor" strokeWidth="3.4" />
|
||||||
|
<path d="M20.4 20.4 27 27" fill="none" stroke="currentColor" strokeLinecap="round" strokeWidth="3.4" />
|
||||||
|
<path d="M10.5 14.7 13.2 17.4 18.8 11.8" fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
{showWordmark && (
|
||||||
|
<span className="text-xl font-black tracking-[-0.035em] text-slate-950">
|
||||||
|
AdSee<span className="text-violet-700">Q</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ export function StoreTrackerManager({ tracked, limit }: { tracked: TrackedStoreI
|
|||||||
<input value={domain} onChange={(event) => setDomain(event.target.value)} required placeholder="ornek-magaza.com" className="min-w-0 flex-1 rounded-2xl border border-slate-200 px-4 py-3" />
|
<input value={domain} onChange={(event) => setDomain(event.target.value)} required placeholder="ornek-magaza.com" className="min-w-0 flex-1 rounded-2xl border border-slate-200 px-4 py-3" />
|
||||||
<button disabled={busy !== null} className="rounded-2xl bg-slate-950 px-5 py-3 font-bold text-white disabled:opacity-50">{busy === "add" ? "Ekleniyor…" : "Mağazayı takip et"}</button>
|
<button disabled={busy !== null} className="rounded-2xl bg-slate-950 px-5 py-3 font-bold text-white disabled:opacity-50">{busy === "add" ? "Ekleniyor…" : "Mağazayı takip et"}</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="mb-5 flex flex-wrap items-center justify-between gap-2 text-sm"><span className="text-slate-500">Yalnızca WinningHunter veritabanında bulunan domainler eklenebilir.</span><span className="rounded-full bg-violet-50 px-3 py-1 font-bold text-violet-700">{tracked.length} / {limit === null ? "∞" : limit}</span></div>
|
<div className="mb-5 flex flex-wrap items-center justify-between gap-2 text-sm"><span className="text-slate-500">Yalnızca AdSeeQ veritabanında bulunan domainler eklenebilir.</span><span className="rounded-full bg-violet-50 px-3 py-1 font-bold text-violet-700">{tracked.length} / {limit === null ? "∞" : limit}</span></div>
|
||||||
{message && <div className={`mb-4 rounded-xl p-3 text-sm font-semibold ${error ? "bg-rose-50 text-rose-700" : "bg-emerald-50 text-emerald-700"}`}>{message}</div>}
|
{message && <div className={`mb-4 rounded-xl p-3 text-sm font-semibold ${error ? "bg-rose-50 text-rose-700" : "bg-emerald-50 text-emerald-700"}`}>{message}</div>}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{tracked.map((item) => <div key={item.id} className="flex flex-col gap-3 rounded-2xl bg-slate-50 p-4 sm:flex-row sm:items-center sm:justify-between"><a href={`/dashboard/stores/${item.store.id}`} className="min-w-0"><div className="truncate font-black">{item.store.name}</div><div className="truncate text-sm text-slate-500">{item.store.domain} · {item.store.products.map((product) => product.title).join(", ") || "Bestseller yok"} · €{item.store.estRevenue30dMax?.toLocaleString() || "—"} est.</div></a><button type="button" disabled={busy !== null} onClick={() => removeStore(item.id)} className="shrink-0 rounded-xl border border-rose-200 px-4 py-2 text-sm font-bold text-rose-700 disabled:opacity-50">{busy === item.id ? "Çıkarılıyor…" : "Takibi bırak"}</button></div>)}
|
{tracked.map((item) => <div key={item.id} className="flex flex-col gap-3 rounded-2xl bg-slate-50 p-4 sm:flex-row sm:items-center sm:justify-between"><a href={`/dashboard/stores/${item.store.id}`} className="min-w-0"><div className="truncate font-black">{item.store.name}</div><div className="truncate text-sm text-slate-500">{item.store.domain} · {item.store.products.map((product) => product.title).join(", ") || "Bestseller yok"} · €{item.store.estRevenue30dMax?.toLocaleString() || "—"} est.</div></a><button type="button" disabled={busy !== null} onClick={() => removeStore(item.id)} className="shrink-0 rounded-xl border border-rose-200 px-4 py-2 text-sm font-bold text-rose-700 disabled:opacity-50">{busy === item.id ? "Çıkarılıyor…" : "Takibi bırak"}</button></div>)}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function sendAlertEmail(email: string, title: string, brand: string) {
|
|||||||
const key = process.env.RESEND_API_KEY?.trim();
|
const key = process.env.RESEND_API_KEY?.trim();
|
||||||
const from = process.env.ALERT_FROM_EMAIL?.trim();
|
const from = process.env.ALERT_FROM_EMAIL?.trim();
|
||||||
if (!key || !from) return false;
|
if (!key || !from) return false;
|
||||||
const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, body: JSON.stringify({ from, to: [email], subject: `${brand}: yeni reklam`, html: `<p><strong>${brand}</strong> için yeni reklam bulundu.</p><p>${title}</p><p><a href="${process.env.NEXT_PUBLIC_APP_URL || ""}/dashboard/brand-tracker">WinningHunter'da görüntüle</a></p>` }) });
|
const response = await fetch("https://api.resend.com/emails", { method: "POST", headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }, body: JSON.stringify({ from, to: [email], subject: `${brand}: yeni reklam`, html: `<p><strong>${brand}</strong> için yeni reklam bulundu.</p><p>${title}</p><p><a href="${process.env.NEXT_PUBLIC_APP_URL || ""}/dashboard/brand-tracker">AdSeeQ'da görüntüle</a></p>` }) });
|
||||||
return response.ok;
|
return response.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user