Initial WinningHunter MVP
This commit is contained in:
+502
@@ -0,0 +1,502 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import {
|
||||
AdScore,
|
||||
AdSource,
|
||||
AdStatus,
|
||||
MediaType,
|
||||
PlanCode,
|
||||
PrismaClient,
|
||||
StorePlatform,
|
||||
UsagePeriod,
|
||||
UserRole
|
||||
} from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const planFeatures = {
|
||||
FREE: {
|
||||
adsSearchDaily: 10,
|
||||
storesSearchDaily: null,
|
||||
tiktokSearchDaily: 0,
|
||||
trendsSearchDaily: 5,
|
||||
apiCreditsMonthly: 0,
|
||||
trackedStores: 0,
|
||||
followedBrands: 0,
|
||||
savedAds: 20,
|
||||
advancedFilters: false,
|
||||
lockedResults: true,
|
||||
exportEnabled: false
|
||||
},
|
||||
BASIC: {
|
||||
adsSearchDaily: 100,
|
||||
storesSearchDaily: null,
|
||||
tiktokSearchDaily: 200,
|
||||
trendsSearchDaily: 20,
|
||||
apiCreditsMonthly: 100,
|
||||
trackedStores: 25,
|
||||
followedBrands: 2,
|
||||
savedAds: 500,
|
||||
advancedFilters: false,
|
||||
lockedResults: false,
|
||||
exportEnabled: true
|
||||
},
|
||||
STANDARD: {
|
||||
adsSearchDaily: 500,
|
||||
storesSearchDaily: null,
|
||||
tiktokSearchDaily: 1000,
|
||||
trendsSearchDaily: 100,
|
||||
apiCreditsMonthly: 20000,
|
||||
trackedStores: 50,
|
||||
followedBrands: 30,
|
||||
savedAds: 2000,
|
||||
advancedFilters: true,
|
||||
lockedResults: false,
|
||||
exportEnabled: true,
|
||||
pinterestAds: true,
|
||||
tiktokAds: true
|
||||
},
|
||||
PREMIUM: {
|
||||
adsSearchDaily: 2000,
|
||||
storesSearchDaily: null,
|
||||
tiktokSearchDaily: null,
|
||||
trendsSearchDaily: null,
|
||||
apiCreditsMonthly: 20000,
|
||||
trackedStores: 500,
|
||||
followedBrands: 200,
|
||||
savedAds: null,
|
||||
advancedFilters: true,
|
||||
lockedResults: false,
|
||||
exportEnabled: true,
|
||||
weeklyResearchCall: true,
|
||||
pinterestAds: true,
|
||||
tiktokAds: true
|
||||
}
|
||||
};
|
||||
|
||||
const stores = [
|
||||
{
|
||||
domain: "petpro-demo.com",
|
||||
name: "PetPro Demo",
|
||||
logoUrl: "https://placehold.co/96x96/7c3aed/ffffff?text=PP",
|
||||
country: "US",
|
||||
currency: "USD",
|
||||
language: "en",
|
||||
niche: "Pets",
|
||||
categoryPath: ["Pets", "Dog Accessories"],
|
||||
theme: "Dawn",
|
||||
visits: 120000,
|
||||
growth: 18.2,
|
||||
revMin: 45000,
|
||||
revMax: 90000,
|
||||
products: [
|
||||
["Smart Dog Collar", 29.99, true],
|
||||
["No Pull Dog Harness", 34.99, true],
|
||||
["Travel Pet Bottle", 19.99, false]
|
||||
],
|
||||
pixels: ["Meta Pixel", "TikTok Pixel", "Google Analytics"],
|
||||
apps: ["Klaviyo", "Judge.me", "ReConvert"]
|
||||
},
|
||||
{
|
||||
domain: "glowly-demo.com",
|
||||
name: "Glowly Demo",
|
||||
logoUrl: "https://placehold.co/96x96/ec4899/ffffff?text=GL",
|
||||
country: "GB",
|
||||
currency: "GBP",
|
||||
language: "en",
|
||||
niche: "Beauty",
|
||||
categoryPath: ["Beauty", "Skincare"],
|
||||
theme: "Prestige",
|
||||
visits: 245000,
|
||||
growth: 27.5,
|
||||
revMin: 90000,
|
||||
revMax: 180000,
|
||||
products: [
|
||||
["LED Face Sculptor", 69.99, true],
|
||||
["Hydrating Serum Kit", 39.99, false],
|
||||
["Ice Roller Pro", 22.99, true]
|
||||
],
|
||||
pixels: ["Meta Pixel", "Pinterest Tag", "Google Analytics"],
|
||||
apps: ["Klaviyo", "Loox", "Recharge"]
|
||||
},
|
||||
{
|
||||
domain: "fitfuel-demo.com",
|
||||
name: "FitFuel Demo",
|
||||
logoUrl: "https://placehold.co/96x96/16a34a/ffffff?text=FF",
|
||||
country: "US",
|
||||
currency: "USD",
|
||||
language: "en",
|
||||
niche: "Supplements",
|
||||
categoryPath: ["Health", "Supplements"],
|
||||
theme: "Impulse",
|
||||
visits: 310000,
|
||||
growth: 9.4,
|
||||
revMin: 130000,
|
||||
revMax: 260000,
|
||||
products: [
|
||||
["Greens Energy Blend", 49.99, true],
|
||||
["Sleep Recovery Gummies", 24.99, true],
|
||||
["Protein Coffee", 39.99, false]
|
||||
],
|
||||
pixels: ["Meta Pixel", "TikTok Pixel", "Google Analytics"],
|
||||
apps: ["Klaviyo", "Recharge", "Yotpo"]
|
||||
},
|
||||
{
|
||||
domain: "homezen-demo.com",
|
||||
name: "HomeZen Demo",
|
||||
logoUrl: "https://placehold.co/96x96/0ea5e9/ffffff?text=HZ",
|
||||
country: "DE",
|
||||
currency: "EUR",
|
||||
language: "de",
|
||||
niche: "Household",
|
||||
categoryPath: ["Household", "Home Gadgets"],
|
||||
theme: "Refresh",
|
||||
visits: 87000,
|
||||
growth: 34.8,
|
||||
revMin: 30000,
|
||||
revMax: 70000,
|
||||
products: [
|
||||
["Magnetic Window Cleaner", 24.99, true],
|
||||
["Foldable Storage Rack", 44.99, false],
|
||||
["Mini Desk Vacuum", 17.99, true]
|
||||
],
|
||||
pixels: ["Meta Pixel", "Google Analytics"],
|
||||
apps: ["Judge.me", "Klaviyo"]
|
||||
}
|
||||
];
|
||||
|
||||
const adCopy = [
|
||||
{
|
||||
domain: "petpro-demo.com",
|
||||
headline: "Smart Dog Collar",
|
||||
text: "Your dog deserves a safer walk. Meet the smart collar loved by 20,000+ pet parents.",
|
||||
media: MediaType.VIDEO,
|
||||
score: AdScore.ESTABLISHED,
|
||||
countries: ["US", "GB", "CA"],
|
||||
days: 27,
|
||||
rank: 8,
|
||||
niche: "Pets"
|
||||
},
|
||||
{
|
||||
domain: "petpro-demo.com",
|
||||
headline: "No Pull Dog Harness",
|
||||
text: "No more pulling. Give your dog freedom while staying in control.",
|
||||
media: MediaType.IMAGE,
|
||||
score: AdScore.HAS_POTENTIAL,
|
||||
countries: ["US"],
|
||||
days: 9,
|
||||
rank: 19,
|
||||
niche: "Pets"
|
||||
},
|
||||
{
|
||||
domain: "glowly-demo.com",
|
||||
headline: "LED Face Sculptor",
|
||||
text: "Spa-level glow at home. See why creators are switching to LED sculpting.",
|
||||
media: MediaType.VIDEO,
|
||||
score: AdScore.ESTABLISHED,
|
||||
countries: ["GB", "US", "AU"],
|
||||
days: 42,
|
||||
rank: 4,
|
||||
niche: "Beauty"
|
||||
},
|
||||
{
|
||||
domain: "glowly-demo.com",
|
||||
headline: "Hydrating Serum Kit",
|
||||
text: "Dry skin? Build a 3-step hydration routine that actually feels lightweight.",
|
||||
media: MediaType.CAROUSEL,
|
||||
score: AdScore.HAS_POTENTIAL,
|
||||
countries: ["GB"],
|
||||
days: 15,
|
||||
rank: 16,
|
||||
niche: "Beauty"
|
||||
},
|
||||
{
|
||||
domain: "fitfuel-demo.com",
|
||||
headline: "Greens Energy Blend",
|
||||
text: "Your morning coffee is not enough. Get clean energy with greens, adaptogens, and no crash.",
|
||||
media: MediaType.VIDEO,
|
||||
score: AdScore.ESTABLISHED,
|
||||
countries: ["US", "CA"],
|
||||
days: 61,
|
||||
rank: 2,
|
||||
niche: "Supplements"
|
||||
},
|
||||
{
|
||||
domain: "fitfuel-demo.com",
|
||||
headline: "Sleep Recovery Gummies",
|
||||
text: "Wake up rested. Sleep Recovery Gummies support your nightly wind-down routine.",
|
||||
media: MediaType.IMAGE,
|
||||
score: AdScore.HAS_POTENTIAL,
|
||||
countries: ["US"],
|
||||
days: 6,
|
||||
rank: 26,
|
||||
niche: "Supplements"
|
||||
},
|
||||
{
|
||||
domain: "homezen-demo.com",
|
||||
headline: "Magnetic Window Cleaner",
|
||||
text: "Clean both sides of your window from inside. The gadget going viral in Europe.",
|
||||
media: MediaType.VIDEO,
|
||||
score: AdScore.HAS_POTENTIAL,
|
||||
countries: ["DE", "AT", "CH"],
|
||||
days: 18,
|
||||
rank: 12,
|
||||
niche: "Household"
|
||||
},
|
||||
{
|
||||
domain: "homezen-demo.com",
|
||||
headline: "Foldable Storage Rack",
|
||||
text: "Small home? Create more space with a foldable storage rack.",
|
||||
media: MediaType.IMAGE,
|
||||
score: AdScore.UNESTABLISHED,
|
||||
countries: ["DE"],
|
||||
days: 3,
|
||||
rank: 49,
|
||||
niche: "Household",
|
||||
inactive: true
|
||||
}
|
||||
];
|
||||
|
||||
function daysAgo(days: number) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - days);
|
||||
return d;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("🌱 Seeding WinningHunter...");
|
||||
|
||||
const free = await prisma.plan.upsert({
|
||||
where: { code: PlanCode.FREE },
|
||||
update: { features: planFeatures.FREE },
|
||||
create: { code: PlanCode.FREE, name: "Free", monthlyPriceEur: 0, quarterlyPriceEur: 0, yearlyPriceEur: 0, features: planFeatures.FREE, sortOrder: 0 }
|
||||
});
|
||||
const basic = await prisma.plan.upsert({
|
||||
where: { code: PlanCode.BASIC },
|
||||
update: { features: planFeatures.BASIC },
|
||||
create: { code: PlanCode.BASIC, name: "Basic", monthlyPriceEur: 42, quarterlyPriceEur: 107, yearlyPriceEur: 302, features: planFeatures.BASIC, sortOrder: 1 }
|
||||
});
|
||||
await prisma.plan.upsert({
|
||||
where: { code: PlanCode.STANDARD },
|
||||
update: { features: planFeatures.STANDARD },
|
||||
create: { code: PlanCode.STANDARD, name: "Standard", monthlyPriceEur: 68, quarterlyPriceEur: 173, yearlyPriceEur: 490, features: planFeatures.STANDARD, sortOrder: 2 }
|
||||
});
|
||||
await prisma.plan.upsert({
|
||||
where: { code: PlanCode.PREMIUM },
|
||||
update: { features: planFeatures.PREMIUM },
|
||||
create: { code: PlanCode.PREMIUM, name: "Premium", monthlyPriceEur: 212, quarterlyPriceEur: 541, yearlyPriceEur: 1526, features: planFeatures.PREMIUM, sortOrder: 3 }
|
||||
});
|
||||
|
||||
const demo = await prisma.user.upsert({
|
||||
where: { email: "demo@winninghunter.local" },
|
||||
update: { passwordHash: await bcrypt.hash("demo1234", 10), name: "Demo User" },
|
||||
create: {
|
||||
email: "demo@winninghunter.local",
|
||||
name: "Demo User",
|
||||
passwordHash: await bcrypt.hash("demo1234", 10),
|
||||
role: UserRole.USER,
|
||||
subscription: { create: { planId: free.id, status: "ACTIVE" } }
|
||||
}
|
||||
});
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "admin@winninghunter.local" },
|
||||
update: { passwordHash: await bcrypt.hash("admin1234", 10), name: "Admin", role: UserRole.ADMIN },
|
||||
create: {
|
||||
email: "admin@winninghunter.local",
|
||||
name: "Admin",
|
||||
passwordHash: await bcrypt.hash("admin1234", 10),
|
||||
role: UserRole.ADMIN,
|
||||
subscription: { create: { planId: basic.id, status: "ACTIVE" } }
|
||||
}
|
||||
});
|
||||
|
||||
for (const s of stores) {
|
||||
const store = await prisma.store.upsert({
|
||||
where: { domain: s.domain },
|
||||
update: {
|
||||
name: s.name,
|
||||
logoUrl: s.logoUrl,
|
||||
country: s.country,
|
||||
currency: s.currency,
|
||||
language: s.language,
|
||||
niche: s.niche,
|
||||
categoryPath: s.categoryPath,
|
||||
shopifyTheme: s.theme,
|
||||
monthlyVisits: s.visits,
|
||||
monthlyVisitGrowth: s.growth,
|
||||
estRevenue30dMin: s.revMin,
|
||||
estRevenue30dMax: s.revMax
|
||||
},
|
||||
create: {
|
||||
domain: s.domain,
|
||||
name: s.name,
|
||||
logoUrl: s.logoUrl,
|
||||
platform: StorePlatform.SHOPIFY,
|
||||
country: s.country,
|
||||
currency: s.currency,
|
||||
language: s.language,
|
||||
niche: s.niche,
|
||||
categoryPath: s.categoryPath,
|
||||
shopUrl: `https://${s.domain}`,
|
||||
createdAtSource: daysAgo(720),
|
||||
dataSince: daysAgo(180),
|
||||
shopifyTheme: s.theme,
|
||||
productCount: s.products.length,
|
||||
collectionCount: 8,
|
||||
trustpilotScore: 4.2 + Math.random() * 0.5,
|
||||
trustpilotReviewCount: Math.floor(400 + Math.random() * 2000),
|
||||
monthlyVisits: s.visits,
|
||||
monthlyVisitGrowth: s.growth,
|
||||
estRevenue30dMin: s.revMin,
|
||||
estRevenue30dMax: s.revMax,
|
||||
estRevenue24hMin: Math.round(s.revMin / 30),
|
||||
estRevenue24hMax: Math.round(s.revMax / 30),
|
||||
trafficCountries: { [s.country]: 68, US: 18, GB: 8, CA: 6 },
|
||||
socials: { instagram: `https://instagram.com/${s.name.toLowerCase().replaceAll(" ", "")}` }
|
||||
}
|
||||
});
|
||||
|
||||
await prisma.brandPage.upsert({
|
||||
where: { source_externalPageId: { source: AdSource.META, externalPageId: `page_${s.domain}` } },
|
||||
update: { storeId: store.id, name: s.name, logoUrl: s.logoUrl, websiteDomain: s.domain },
|
||||
create: {
|
||||
source: AdSource.META,
|
||||
externalPageId: `page_${s.domain}`,
|
||||
name: s.name,
|
||||
logoUrl: s.logoUrl,
|
||||
websiteDomain: s.domain,
|
||||
pageUrl: `https://facebook.com/${s.domain.replaceAll(".", "")}`,
|
||||
fbLikes: Math.round(s.visits / 8),
|
||||
igFollowers: Math.round(s.visits / 5),
|
||||
country: s.country,
|
||||
niche: s.niche,
|
||||
storeId: store.id
|
||||
}
|
||||
});
|
||||
|
||||
for (const [title, price, best] of s.products) {
|
||||
const handle = String(title).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
await prisma.storeProduct.upsert({
|
||||
where: { id: `${store.id}_${handle}` },
|
||||
update: { title: String(title), price: Number(price), isBestSeller: Boolean(best) },
|
||||
create: {
|
||||
id: `${store.id}_${handle}`,
|
||||
storeId: store.id,
|
||||
title: String(title),
|
||||
handle,
|
||||
imageUrl: `https://placehold.co/320x240/f1f5f9/0f172a?text=${encodeURIComponent(String(title))}`,
|
||||
productUrl: `https://${s.domain}/products/${handle}`,
|
||||
price: Number(price),
|
||||
compareAtPrice: Number(price) * 1.6,
|
||||
currency: s.currency,
|
||||
variantCount: 3,
|
||||
isBestSeller: Boolean(best),
|
||||
firstSeenAt: daysAgo(90)
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const pixel of s.pixels) {
|
||||
await prisma.storePixel.upsert({ where: { storeId_type_value: { storeId: store.id, type: pixel, value: "" } }, update: {}, create: { storeId: store.id, type: pixel, value: "" } });
|
||||
}
|
||||
for (const app of s.apps) {
|
||||
await prisma.storeApp.upsert({ where: { storeId_name: { storeId: store.id, name: app } }, update: {}, create: { storeId: store.id, name: app, category: "Marketing" } });
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const date = daysAgo(i * 30);
|
||||
await prisma.storeSnapshot.upsert({
|
||||
where: { storeId_date: { storeId: store.id, date } },
|
||||
update: {},
|
||||
create: { storeId: store.id, date, monthlyVisits: Math.round(s.visits * (1 - i * 0.06)), estRevenueMin: Math.round(s.revMin * (1 - i * 0.05)), estRevenueMax: Math.round(s.revMax * (1 - i * 0.05)), activeAdsCount: Math.max(2, 22 - i * 2), productCount: s.products.length }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let adIndex = 1;
|
||||
for (const a of adCopy) {
|
||||
const page = await prisma.brandPage.findFirstOrThrow({ where: { websiteDomain: a.domain } });
|
||||
const ad = await prisma.ad.upsert({
|
||||
where: { source_externalAdId: { source: AdSource.META, externalAdId: `demo_ad_${adIndex}` } },
|
||||
update: {},
|
||||
create: {
|
||||
source: AdSource.META,
|
||||
externalAdId: `demo_ad_${adIndex}`,
|
||||
brandPageId: page.id,
|
||||
status: a.inactive ? AdStatus.INACTIVE : AdStatus.ACTIVE,
|
||||
mediaType: a.media,
|
||||
adScore: a.score,
|
||||
primaryText: a.text,
|
||||
headline: a.headline,
|
||||
description: "Demo competitive intelligence ad.",
|
||||
ctaText: "Shop Now",
|
||||
landingUrl: `https://${a.domain}`,
|
||||
productUrl: `https://${a.domain}/products/${a.headline.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
|
||||
language: a.domain.includes("homezen") ? "de" : "en",
|
||||
countries: a.countries,
|
||||
niche: a.niche,
|
||||
firstSeenAt: daysAgo(a.days),
|
||||
lastSeenAt: a.inactive ? daysAgo(5) : new Date(),
|
||||
createdAtSource: daysAgo(a.days),
|
||||
daysRunning: a.days,
|
||||
estimatedReachMin: 10000 * adIndex,
|
||||
estimatedReachMax: 40000 * adIndex,
|
||||
estimatedSpendMin: 500 * adIndex,
|
||||
estimatedSpendMax: 1600 * adIndex,
|
||||
estimatedCpm: 12 + adIndex,
|
||||
rank: 100 - a.rank,
|
||||
rankPercentile: a.rank,
|
||||
rankGrowth: a.rank < 20 ? "Rising" : "Stable"
|
||||
}
|
||||
});
|
||||
await prisma.adCreative.create({
|
||||
data: {
|
||||
adId: ad.id,
|
||||
type: a.media,
|
||||
url: `https://placehold.co/640x480/111827/ffffff?text=${encodeURIComponent(a.headline)}`,
|
||||
thumbnailUrl: `https://placehold.co/640x480/111827/ffffff?text=${encodeURIComponent(a.headline)}`,
|
||||
durationSeconds: a.media === MediaType.VIDEO ? 30 + adIndex : null
|
||||
}
|
||||
});
|
||||
for (const [type, value] of [
|
||||
["hook", a.text.split(".")[0]],
|
||||
["angle", `${a.niche} winning angle`],
|
||||
["persona", `${a.niche} buyers`],
|
||||
["emotion", a.rank < 15 ? "Urgency" : "Trust"]
|
||||
]) {
|
||||
await prisma.aITag.create({ data: { adId: ad.id, type, value, confidence: 0.91, model: "seed-demo", promptVersion: "v0" } });
|
||||
}
|
||||
adIndex++;
|
||||
}
|
||||
|
||||
const firstAd = await prisma.ad.findFirstOrThrow();
|
||||
const folder = await prisma.savedFolder.upsert({
|
||||
where: { userId_name: { userId: demo.id, name: "Kazanan Pet Reklamları" } },
|
||||
update: {},
|
||||
create: { userId: demo.id, name: "Kazanan Pet Reklamları", color: "#22c55e" }
|
||||
});
|
||||
await prisma.savedAd.upsert({
|
||||
where: { userId_adId: { userId: demo.id, adId: firstAd.id } },
|
||||
update: { folderId: folder.id },
|
||||
create: { userId: demo.id, adId: firstAd.id, folderId: folder.id, note: "Demo kayıt." }
|
||||
});
|
||||
const firstStore = await prisma.store.findFirstOrThrow();
|
||||
await prisma.trackedStore.upsert({
|
||||
where: { userId_storeId: { userId: admin.id, storeId: firstStore.id } },
|
||||
update: {},
|
||||
create: { userId: admin.id, storeId: firstStore.id, notes: "Demo takip." }
|
||||
});
|
||||
await prisma.usageCounter.upsert({
|
||||
where: { userId_metric_periodKey: { userId: demo.id, metric: "ads_search_daily", periodKey: new Date().toISOString().slice(0, 10) } },
|
||||
update: { used: 1, limit: 10 },
|
||||
create: { userId: demo.id, metric: "ads_search_daily", period: UsagePeriod.DAILY, periodKey: new Date().toISOString().slice(0, 10), used: 1, limit: 10 }
|
||||
});
|
||||
await prisma.ingestJob.create({ data: { source: "seed", type: "demo-data", status: "COMPLETED", startedAt: new Date(), finishedAt: new Date(), recordsImported: 18 } });
|
||||
|
||||
console.log("✅ Demo: demo@winninghunter.local / demo1234");
|
||||
console.log("✅ Admin: admin@winninghunter.local / admin1234");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user