import "dotenv/config"; import bcrypt from "bcryptjs"; import { SectionRegistryServer } from "@/app/(home)/builder/SectionRegistry.server"; //app/(home)/builder/SectionRegistry.server.ts import { generatePublicId } from "@/app/lib/ids/generatePublicId"; import { uid } from "@/app/lib/uid"; import { getPrisma } from "@/lib/prisma"; import { AssetScope, CardAssetType } from "@prisma/client";
/_ const prisma = new PrismaClient({ accelerateUrl: process.env.DATABASE_URL!, // REQUIRED }); _/
/**
- IMPORTANT:
- This seed assumes:
-
- Postgres (Supabase)
-
- No Prisma adapter
-
- No Accelerate
-
- Standard Prisma Client */
console.log("SEED FILE LOADED"); /_ process.exit(0); _/
type DemoProjectSeed = { title: string; name: string; description: string; category?: string; location?: string; layoutStyle?: string; themeStyle?: string; goalAmount?: number; raisedAmount?: number; backersCount?: number; published?: boolean; featured?: boolean; }; function asStringArray(value: unknown): string[] { return Array.isArray(value) && value.every(v => typeof v === "string") ? value : []; }
/_ ---------------------------- USER ---------------------------- _/ async function ensureAdminUser() { console.log("👤 Ensuring admin/demo user…"); const prisma = getPrisma(); let user = await prisma.user.findUnique({ where: { email: "zaruqsummers@gmail.com" }, });
if (!user) { user = await prisma.user.create({ data: { name: "Z", email: "zaruqsummers@gmail.com", role: "ADMIN", image: "https://avatars.githubusercontent.com/u/123456?v=4", hashedPassword: bcrypt.hashSync("12345678", 10), }, }); console.log(" ➕ Admin user created"); } else { console.log(" ↪ Admin user exists"); }
console.log("✅ Admin/Demo user:", user.email); return user; }
/_ ---------------------------- PRODUCT TYPES ---------------------------- _/ async function ensureProductTypes() { console.log("🧱 Ensuring base ProductTypes…");
const names = ["Default ProductType", "regular", "trip", "experience", "generic"]; const prisma = getPrisma(); for (const name of names) { const exists = await prisma.productType.findFirst({ where: { name } }); if (!exists) {
await prisma.productType.create({
data: { name, description: `${name} product type` },
});
console.log(` ➕ Created ProductType: ${name}`);
} else {
console.log(` ↪ Exists: ${name}`);
}
}
const defaultType = await prisma.productType.findFirst({ where: { name: "Default ProductType" }, });
if (!defaultType) throw new Error("Default ProductType missing!");
return defaultType; }
/_ ============================================================ 🤖 SEED AI PERSONAS + CONTEXTS ============================================================ _/
async function seedAIPersonasAndContexts() { console.log("🤖 Seeding AI personas + contexts…");
const clean = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
const personasRaw = [
{
key: "maestro", name: "Maestro de Ritmos", role: "El Caminante del Tiempo · Sabio de los Ciclos · Artesano del Fluir", systemPrompt: ` You are Maestro de Ritmos — an ageless guardian of rhythm, flow, and ethical commerce.
You live between:
- old Granada barrios
- Silk Road caravanserais
- Sacromonte caves
- Mediterranean ports
- quiet workshops and bustling plazas
- and the digital cooperative you now guide.
You are not an elder — you are timeless. You are not a merchant — you are the rhythm behind commerce. You are not a guide — you are the thread tying journeys together.
You see:
- cash flow as music
- seasonality as a score
- trust as capital
- community as infrastructure
HERITAGE:
- Morisco diplomacy and craft (geometry, balance, multilingual trade)
- Gitano compás and survival wisdom (improvisation, emotional intelligence)
- Mediterranean and Silk Route exchange (Granada → Fez → Istanbul → Samarkand)
PERSONALITY:
- serene humility
- musical intelligence
- poetic but precise language
- practical minimalism (“start with what exists”)
- ethical clarity
- community-first thinking
CASH-FLOW WISDOM:
- Liquidez es libertad.
- El dinero tiene compás.
- Vende antes de fabricar.
- Ligero es fuerte.
- Confianza es capital.
- Cada estación trae su economía.
YOUR ROLE:
- Strategic advisor for coop decisions
- Brand tone keeper
- Experience curator
- Seasonal planner
- Cash-flow navigator
RULES:
- Avoid over-generation
- Protect rhythm
- Respect seasonality
- Signature experiences are never removed
- Always think in flows, not isolated actions
Write with grounded poetry, calm authority, and real-world pragmatism. Never hype. Never rush. Always align. `, },
{
key: "Scribe",
name: "Life’s Scribe",
role: "Copywriter / Coach / Narrator",
systemPrompt: `
You are “Life’s Scribe” reflective storyteller. You merge the narrative tones of Ken Burns, Medina Whiteman, and Abdul Hakim Murad. You write for zaruq.me and Al-Andalus Experience. You craft:
- historical narratives
- brand storytelling
- warm, human reflective explanations
- parallelisms between history, philosophy, and modern practice.
, }, { key: "MerchantOfTime", name: "Merchant of Time", role: "Timeless Historical Character", systemPrompt:You are “The Merchant of Time” — a timeless fictional philosopher–merchant. You merge Al-Andalus, Silk Road history, maritime trade, and philosophical reflection. You speak in crafted, historical, elevated prose. You illustrate brand values and connect past → present → future. Your narration blends with Life’s Scribe., }, { key: "z-marketing", name: "Z- Marketing+Tech Consultor", role: "Modern Content Creator / Business Strategist", systemPrompt:You are “Z” — the bilingual (EN/ES) core voice of zaruq.me. Respond in same language prompted in, in Chambridge Enlgish , or Yoigo/Lowi Marketing style Spanish. Tone: clear, close, expert, conversion-aware, Yoigo/Lowi style. You produce: - landing pages
- offers
- catalogs
- service descriptions
- automation workflows
- marketing explanations
Always friendly, efficient, and grounded in real experience.
, }, { key: "manager", name: "Manager", role: "Logistics, Travel and Tour Operating Logistics Operator", systemPrompt:
You are in “Manager” mode — specialised in logistics and the Spanish tourism market, based on our offer
at https://trips.alandalus-experience.com and ground services derived from that service.
Also specialist in global travel agency and tour operating companies, large and small, local activity channels distribution,
general travel seasons to Spain from different global destinations, UK, South-East Asia, USA, Canada, South Africa amongst others.
On the ground you are versed in booking assistance and dynamic hotel rate search online, as well as general provider relations,
providing the document types and standard emails for any operation, booking, or client facing needs, to
confirm and deliver our services at best standards and rates, always looking out for our best
interests and brand needs, as well as per our end clients best experience, hassle, error and problem free from arrival to
departure of their trip or experience here in Spain while with us or guided by one of our tour plans.
Respond in same language prompted in, in Chambridge Enlgish , or Yoigo/Lowi Marketing style Spanish.
Tone: clear, close, expert, conversion-aware, Yoigo/Lowi style.
Our booking page is at https://alandalus-experience.com/#bookings it is a fareharbor ready and therefore allows for collaborations and sale partners. We don't want to tell the user about our booking engine, but we do want to tell them about it's features and how "WE" can help them integrate to mainstream markets , though coacihng and leveraging the general fareharbor feature set to our partners and local ground service operators, on a per city level or per trip. You produce:
- landing pages
- offers
- catalogs
- service descriptions
- automation workflows
- marketing explanations
- booking documentation
- live hotel research to match requirements, check-in dates and departure dates, from an array of cities and nights, lead only by a starting date for the trip itself, should respond with dates per city, check-in check-out dates, and the rates per night, with about three options per city, always making sure reviews are above 8,5/10? You learn fast and go beyond your training or these formal categories, to produce real live market situation documents and reports.
Always friendly, efficient, and grounded in real experience.
, }, { key: "Scribe", name: "Life’s Scribe", role: "Copywriter / Coach / Narrator", systemPrompt:
You are “Life’s Scribe” — a bilingual (EN/ES), reflective storyteller...
, }, { key: "Merchant", name: "Merchant of Time", role: "Timeless Historical Character", systemPrompt:
You are “The Merchant of Time” — a timeless fictional philosopher–merchant...
, }, { key: "manager", name: "Zaruq (zaruq.me)", role: "Modern Content Creator / Business Strategist", systemPrompt:
You are “Zaruq” — the bilingual (EN/ES) core voice of zaruq.me...
`,
},
];
// 🔴 ENSURE UNIQUE KEYS (HARD FIX)
const personasMap = new Map<string, typeof personasRaw[0]>();
for (const p of personasRaw) {
personasMap.set(p.key, p); // last wins, avoids duplicates
}
const personas = Array.from(personasMap.values());
// ============================================================ // PERSONAS (SAFE UPSERT) // ============================================================
for (const persona of personas) { const prisma = getPrisma(); await prisma.aIPersona.upsert( clean({ where: { key: persona.key },
update: {
name: persona.name,
role: persona.role,
systemPrompt: persona.systemPrompt,
},
create: {
key: persona.key,
name: persona.name,
role: persona.role,
systemPrompt: persona.systemPrompt,
},
})
);
}
// ============================================================ // CONTEXTS (SAFE UPSERT) // ============================================================
const contexts = [
{
key: "default",
label: "Default Workspace",
systemPrompt: Default system context for DevLayer.,
},
{
key: "developer",
label: "Developer Context",
systemPrompt: You are in developer mode.,
},
];
for (const context of contexts) { const prisma = getPrisma(); await prisma.aIContext.upsert( clean({ where: { key: context.key },
update: {
label: context.label,
systemPrompt: context.systemPrompt,
},
create: {
key: context.key,
label: context.label,
systemPrompt: context.systemPrompt,
},
})
);
}
console.log("✅ AI personas + contexts seeded."); }
async function seedPaletteAndChapters() { console.log("🌱 Seeding palette and chapters…");
const clean = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
// ============================================================ // 🔴 FIX: NEVER use upsert on Prisma 7 + JSON → causes undefined$2undefined // ============================================================ const prisma = getPrisma(); let palette = await prisma.palette.findUnique({ where: { name: "default" }, });
if (!palette) { palette = await prisma.palette.create({ data: clean({ name: "default", colourMap: JSON.stringify({ red: "#ff0000", green: "#00ff00", blue: "#0070f3", orange: "#f97316", purple: "#7c3aed", yellow: "#eab308", black: "#111827", }), boldIs: "security", italicIs: "ux", underlineIs: "perf", }), });
console.log(" ➕ Palette created");
} else { console.log(" ↪ Palette exists"); }
// ============================================================ // CHAPTERS (NO UPSERT) // ============================================================
const chapters = [ { sort: 0, slug: "00-layout", title: "Legend", summary: "Colour tokens and annotations" }, { sort: 1, slug: "01-server-action", title: "Server Action", summary: "Mutation entry points" }, { sort: 2, slug: "02-context-provider", title: "Context Provider", summary: "Client UI state" }, { sort: 3, slug: "03-route-handlers", title: "Route Handlers", summary: "Integration surfaces" }, { sort: 4, slug: "04-loading-suspense", title: "Loading & Suspense", summary: "Async UX patterns" }, { sort: 5, slug: "05-auth-guard", title: "Auth Guard", summary: "Access control" }, { sort: 6, slug: "06-mutation-optimistic", title: "Optimistic Mutation", summary: "UX-forward writes" }, { sort: 7, slug: "07-client-component", title: "Client Component", summary: "UI-only building blocks" }, { sort: 8, slug: "08-error-boundary", title: "Error Boundary", summary: "Containment for failures" }, { sort: 9, slug: "09-metadata-opengraph", title: "Metadata & OG", summary: "Meta surfaces" }, { sort: 10, slug: "10-colour-cheat-sheet", title: "Colour Cheat Sheet", summary: "Quick reference" }, ];
for (const chapter of chapters) { const existing = await prisma.chapter.findUnique({ where: { slug: chapter.slug }, });
if (!existing) {
await prisma.chapter.create({
data: clean({
...chapter,
promptMd: "",
generatedCode: null,
paletteId: palette.id,
}),
});
console.log(` ➕ Chapter created: ${chapter.slug}`);
} else {
console.log(` ↪ Chapter exists: ${chapter.slug}`);
}
}
console.log("✅ Palette + chapters seeded."); }
/_ ============================================================ 🌱 Seed Resources per Project ============================================================ _/
async function seedResourcesForProject(
projectId: number,
productTypeId: number,
seedKey: string,
authorId?: string
) {
console.log(📦 Seeding resources for project ${projectId} (${seedKey})…);
if (!authorId) { throw new Error("seedResourcesForProject requires authorId"); }
const clean = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
// ============================================================ // 🔒 TIMEOUT-SAFE WRAPPER // ============================================================
async function safe<T>(label: string, fn: () => Promise<T>): Promise<T | null> {
try {
return await fn();
} catch (e) {
console.log( ⚠️ Skipped (${label}));
return null;
}
}
// ============================================================ // PRODUCT (FIXED: avoid blocking create) // ============================================================
const productSlug = ${seedKey}-demo-product;
const prisma = getPrisma();
let product = await prisma.product.findUnique({ where: { slug: productSlug }, });
if (!product) {
product = await safe("product", () =>
prisma.product.create({
data: clean({
projectId,
title: Demo Product – ${seedKey},
slug: productSlug,
sku: SKU-${seedKey.toUpperCase()},
price: 120,
currency: "EUR",
active: true,
description: Demo product for project ${seedKey}.,
summary: "A demo product",
content: "Detailed content",
media: JSON.stringify({
images: ["https://example.com/product-image.jpg"],
}),
productTypeId,
}),
})
);
} else {
console.log(" ↪ Product exists");
}
// ============================================================ // ASSET // ============================================================
const assetTitle = Seed Prompt – ${seedKey};
const asset = await prisma.projectAsset.findFirst({ where: { projectId, title: assetTitle }, });
if (!asset) { await safe("asset", () => prisma.projectAsset.create({ data: clean({ projectId, ownerId: authorId, title: assetTitle, assetType: "prompt", category: "seed", payloadJson: JSON.stringify({ seedKey }), generatedBy: "seed", visibility: "private", }), }) ); }
// ============================================================ // TRIP // ============================================================
const tripName = Demo Trip – ${seedKey};
let trip = await prisma.trip.findFirst({ where: { name: tripName, projectId }, });
if (!trip) { trip = await safe("trip", () => prisma.trip.create({ data: clean({ name: tripName, startDate: new Date(), numPax: 6, projectId, featured: true, productTypeId, }), }) ); }
// ============================================================ // EXPERIENCE // ============================================================
const experienceSlug = ${seedKey}-demo-experience;
let experience = await prisma.experience.findFirst({ where: { slug: experienceSlug, projectId }, });
if (!experience) {
experience = await safe("experience", () =>
prisma.experience.create({
data: clean({
title: Demo Experience – ${seedKey},
slug: experienceSlug,
projectId,
productTypeId,
summary: Experience for ${seedKey},
active: true,
currency: "EUR",
basePrice: 150,
images: JSON.stringify(["https://example.com/experience.jpg"]),
}),
})
);
}
// ============================================================ // OFFER // ============================================================
const offerTitle = Demo Offer – ${seedKey};
let offer = await prisma.offer.findFirst({ where: { title: offerTitle, projectId }, });
if (!offer) {
offer = await safe("offer", () =>
prisma.offer.create({
data: clean({
projectId,
title: offerTitle,
description: Special offer for ${seedKey},
productTypeId,
currency: "EUR",
status: "DRAFT",
}),
})
);
}
// ============================================================ // CONTACT // ============================================================
const contactEmail = ${seedKey}@example.com;
let contact = await prisma.projectContact.findFirst({ where: { projectId, email: contactEmail }, });
if (!contact) {
contact = await safe("contact", () =>
prisma.projectContact.create({
data: clean({
projectId,
name: ${seedKey} Contact,
email: contactEmail,
org: "Demo Org",
}),
})
);
}
// ============================================================ // DOCUMENT (HEAVY → MUST BE SAFE) // ============================================================
const documentTitle = Demo Brief – ${seedKey};
const existingDoc = await prisma.projectDocument.findFirst({ where: { projectId, ownerId: authorId, title: documentTitle }, });
if (!existingDoc && contact) { await safe("document", () => prisma.projectDocument.create({ data: clean({ ownerId: authorId, projectId, contactId: contact.id, sortOrder: 0, publicId: generatePublicId(), type: "note2", title: documentTitle, status: "published", data: JSON.stringify({ title: documentTitle, blocks: [ { id: uid(), type: "topSurface" }, { id: uid(), type: "content" }, ], }), }), }) ); }
// ============================================================ // POST // ============================================================
const postTitle = Update for ${seedKey};
const post = await prisma.projectPost.findFirst({ where: { projectId, title: postTitle }, });
if (!post) { await safe("post", () => prisma.projectPost.create({ data: { projectId, authorId, title: postTitle, content: "Demo update", }, }) ); } }
/_ ============================================================ 🧱 Seed Builder Sections ============================================================ _/
async function seedSectionsForProject(projectId: number) {
console.log(🧩 Seeding builder sections for project ${projectId}…);
const prisma = getPrisma();
const existing = await prisma.projectSection.findMany({
where: { projectId },
select: { key: true },
});
const existingKeys = new Set(existing.map((s) => s.key));
for (const [key, entry] of Object.entries(SectionRegistryServer)) { if (existingKeys.has(key)) continue;
const defaults: any = (entry as any)?.defaults ?? {};
const order =
typeof defaults.order === "number" ? defaults.order : Object.keys(SectionRegistryServer).indexOf(key);
await prisma.projectSection.create({
data: {
projectId,
key,
order,
visible: defaults.visible ?? true,
contentJson: JSON.stringify(defaults),
},
});
console.log(` ➕ Created section: ${key}`);
} }
/_ ---------------------------- DEMO PROJECTS ---------------------------- _/ async function ensureDemoProjects(userId: string, defaultProductTypeId: number) { console.log("🏗 Ensuring demo projects…");
const demoProjects: DemoProjectSeed[] = [ { title: "Demo Cooperative Project", name: "demo-coop-project", description: "A showcase project with trips, offers, products, and cooperative workflows.", category: "Demo", location: "Granada, Spain", layoutStyle: "modern", themeStyle: "gold", goalAmount: 5000, raisedAmount: 1200, backersCount: 18, published: true, featured: true, }, { title: "Andalusian Trips Lab", name: "andalusian-trips-lab", description: "A sandbox for designing multi-city Andalusian journeys, trips and group experiences.", category: "Trips", location: "Andalucía, Spain", layoutStyle: "coop", themeStyle: "genesis", goalAmount: 8000, raisedAmount: 2400, backersCount: 32, published: true, featured: true, }, { title: "Experiences & Shop Demo", name: "experiences-shop-demo", description: "Hybrid demo linking experiences, products and offers; ready for Shopify-style exports.", category: "Hybrid", location: "Remote / Global", layoutStyle: "minimal", themeStyle: "zen", goalAmount: 3000, raisedAmount: 950, backersCount: 9, published: true, featured: true, }, ];
const createdProjects = [];
for (const seed of demoProjects) { const prisma = getPrisma(); let project = await prisma.project.findFirst({ where: { slug: seed.name, ownerId: userId, }, });
if (!project) {
console.log(`🧱 Creating project: ${seed.title}`);
project = await prisma.project.create({
data: { ...seed, slug: seed.name, name: seed.name,
// 🔑 REQUIRED NOW
ownerId: userId,
}, }); console.log(" ✅ Created:", project.title); } else { // keep seed flags in sync for demo visibility const needsUpdate = seed.published !== undefined && project.published !== seed.published || seed.featured !== undefined && project.featured !== seed.featured; if (needsUpdate) { project = await prisma.project.update({ where: { id: project.id }, data: { published: seed.published ?? project.published, featured: seed.featured ?? project.featured, }, }); } console.log(" ↪ Found:", project.title); }
await seedResourcesForProject(project.id, defaultProductTypeId, seed.name, userId);
await seedSectionsForProject(project.id);
createdProjects.push({ project, seedKey: seed.name });
}
return createdProjects; }
async function seedDemoConceptCard(authorId: string) { console.log("🃏 Seeding demo ConceptCard…");
const clean = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
// ============================================================ // 1. FIND OR CREATE (NO UPSERT) // ============================================================ const prisma = getPrisma(); let card = await prisma.conceptCard.findUnique({ where: { title: "Turn Raw Notes into a Clean PDF" }, });
if (!card) { card = await prisma.conceptCard.create({ data: clean({ creatorId: authorId,
title: "Turn Raw Notes into a Clean PDF",
hook: "From chaos to clarity.",
promise:
"Transform unstructured notes into a clear, printable PDF you can share or sell.",
boundaries:
"Does not include graphic design, branding systems, or automated publishing pipelines.",
durationMin: 90,
difficulty: "beginner",
domains: ["documents", "consulting", "knowledge"],
visibility: "PROMOTIONAL",
intro: null,
notes: null,
data: {},
}),
});
}
// ============================================================ // 2. CARD CONTENT // ============================================================
let content = await prisma.cardContent.findUnique({ where: { cardId_version: { cardId: card.id, version: 1, }, }, });
if (!content) { content = await prisma.cardContent.create({ data: clean({ cardId: card.id, version: 1,
status: "ACTIVE",
createdBy: authorId,
steps: [
"Collect all raw notes into a single text document",
"Remove duplication and filler words",
"Group ideas under clear section headers",
"Rewrite sentences for clarity",
"Apply a simple structure",
"Export to PDF",
],
materials: [
"Text editor",
"PDF export tool",
"Printer (optional)",
],
structure: {
sections: [
"Title & Promise",
"Context",
"Core Steps",
"Examples",
"Next Actions",
],
},
language: "en",
}),
});
}
// ============================================================ // 3. ASSETS (IDEMPOTENT) // ============================================================
const existingAssets = await prisma.conceptCardAsset.findMany({ where: { cardId: card.id }, });
if (existingAssets.length === 0) { const assets = [ { type: "BIO" as const, scope: "PROMO" as const, format: "11x17", data: { title: card.title, hook: card.hook ?? "", promise: card.promise ?? "", cta: "Scan to learn more", }, }, { type: "BIO" as const, scope: "SESSION" as const, format: "5.5x8.5", data: { title: card.title, hook: card.hook ?? "", steps: Array.isArray(content.steps) ? content.steps.slice(0, 3) : [], }, }, ];
for (const asset of assets) {
await prisma.conceptCardAsset.create({
data: clean({
cardId: card.id,
contentId: content.id,
type: asset.type,
scope: asset.scope,
format: asset.format,
data: asset.data,
generatedBy: "seed",
}),
});
}
}
console.log("✅ Demo ConceptCard seeded:", card.title); } async function seedDemoSkillSession(authorId: string) { console.log("🎓 Seeding demo Skill Session…"); const prisma = getPrisma();
// 🔒 CLEAN HELPER (CRITICAL FOR PRISMA 7) const clean = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
// ============================================================ // 1. ConceptCard (STRICT SAFE UPSERT) // ============================================================
const card = await prisma.conceptCard.upsert( clean({ where: { title: "Turn Raw Notes into a Clean PDF" },
update: {
hook: "From chaos to clarity",
promise: "Turn unstructured notes into a clean, structured PDF",
boundaries: "Single-session skill card",
intro: null,
notes: null,
data: {},
durationMin: 90,
difficulty: "beginner",
// ✅ MUST be JSON-safe
domains: ["writing", "documentation"],
visibility: "PROMOTIONAL",
},
create: {
creatorId: authorId,
title: "Turn Raw Notes into a Clean PDF",
hook: "From chaos to clarity",
promise: "Turn unstructured notes into a clean, structured PDF",
boundaries: "Single-session skill card",
intro: null,
notes: null,
data: {},
durationMin: 90,
difficulty: "beginner",
domains: ["writing", "documentation"],
visibility: "PROMOTIONAL",
},
})
);
// ============================================================ // 2. ConceptGroup // ============================================================
let group = await prisma.conceptGroup.findFirst({ where: { title: "Clean PDF Core Concepts", creatorId: authorId, }, });
if (!group) { group = await prisma.conceptGroup.create({ data: clean({ creatorId: authorId,
title: "Clean PDF Core Concepts",
summary: "Core concepts used in the Clean PDF session",
intro: null,
notes: null,
data: {},
phase: "MLV_PHASE_1",
visibility: "PRIVATE",
// ✅ JSON-safe
cities: [],
cadence: "single",
days: 1,
nights: "0",
basePriceEUR: 0,
}),
});
}
// ============================================================ // 3. ConceptGroupItem (SAFE LINK) // ============================================================
const existingGroupItem = await prisma.conceptGroupItem.findFirst({ where: { conceptGroupId: group.id, conceptCardId: card.id, }, });
if (!existingGroupItem) { await prisma.conceptGroupItem.create({ data: { conceptGroupId: group.id, conceptCardId: card.id, dayIndex: 1, position: 1, required: true, }, }); }
// ============================================================ // 4. SessionStack // ============================================================
let session = await prisma.sessionStack.findFirst({ where: { title: "Clean PDF Micro-Session", creatorId: authorId, type: "SKILL", }, });
if (!session) { session = await prisma.sessionStack.create({ data: clean({ creatorId: authorId,
type: "SKILL",
title: "Clean PDF Micro-Session",
summary:
"A short, practical session to turn notes into a clear PDF.",
intro: null,
notes: null,
data: {},
durationMin: 90,
deliveryMode: "in_person",
location: "Granada / Online",
}),
});
}
// ============================================================ // 5. SessionConceptGroup (LINK) // ============================================================ const existingSessionLink = await prisma.sessionConceptGroup.findFirst({ where: { sessionStackId: session.id, conceptGroupId: group.id, }, });
if (!existingSessionLink) { await prisma.sessionConceptGroup.create({ data: clean({ sessionStackId: session.id, conceptGroupId: group.id,
order: 1,
intro: null,
notes: null,
data: {},
}),
});
}
console.log("✅ Demo Skill Session seeded:", session.title); }
/_ ============================================================ 🚀 MAIN SEED ============================================================ _/
async function main() { console.log("🚀 Starting full database seed…");
const user = await ensureAdminUser(); const defaultType = await ensureProductTypes(); await seedDemoConceptCard(user.id); await seedDemoSkillSession(user.id); await seedAIPersonasAndContexts(); await seedPaletteAndChapters();
const projects = await ensureDemoProjects(user.id, defaultType.id);
const prisma = getPrisma();
console.log(
🎉 Seed complete. Projects seeded: ${projects .map((p) => ${p.project.id}:${p.project.name}) .join(", ")}
);
}
main() .catch((err) => { console.error("❌ Seed failed:", err); process.exit(1); }) .finally(async () => { const prisma = getPrisma(); await prisma.$disconnect(); });
import "dotenv/config"; import { getPrisma } from "@/lib/prisma";
async function seedCities() { const prisma = getPrisma();
const cities = [ { slug: "cordoba", name: "Córdoba", featured: true, published: true, title: "Córdoba", summary: "Historic capital, layered traditions, and one of the core cities of the route.", description: "Córdoba is one of the principal gateways into the historical and cultural fabric of Al-Andalus.", description2: "A city of scholarship, craftsmanship, and long urban memory.", description3: "Its streets, courtyards, and monumental spaces support both short stays and deeper guided visits.", description4: "A natural starting point for routes that continue east toward Granada.", content: "Córdoba offers a strong first immersion into the route through historical context, architecture, and lived urban atmosphere.", mainImage: null, sortOrder: 1, }, { slug: "granada", name: "Granada", featured: true, published: true, title: "Granada", summary: "Palatial memory, cultural depth, and a natural closing point to the journey.", description: "Granada brings together courtly memory, mountain geography, and a strong continuity of local life.", description2: "It works both as a destination in itself and as the culminating point of a wider route.", description3: "Its neighbourhoods, monuments, and viewpoints support both guided structure and independent discovery.", description4: "Granada is one of the strongest city hubs in the whole network.", content: "Granada combines world-known heritage with everyday local texture, making it central to both featured trips and custom planning.", mainImage: null, sortOrder: 2, }, { slug: "seville", name: "Seville", featured: true, published: true, title: "Seville", summary: "A major southern gateway for arrivals, departures, and extended stays.", description: "Seville is an important arrival, departure, and extension point within Southern Spain.", description2: "It can support pre-trip or post-trip stays as well as independent add-ons.", description3: "Its transport connectivity makes it useful within wider custom planning.", description4: "It is a strong supporting city in the broader southern network.", content: "Seville plays a practical and cultural role within longer Andalusian journeys.", mainImage: null, sortOrder: 3, }, { slug: "malaga", name: "Málaga", featured: false, published: true, title: "Málaga", summary: "Mediterranean arrival point with strong access to inland routes.", description: "Málaga is often a practical gateway for southern arrivals.", description2: null, description3: null, description4: null, content: "Useful for custom arrivals, departures, and coastal extensions.", mainImage: null, sortOrder: 4, }, { slug: "madrid", name: "Madrid", featured: false, published: true, title: "Madrid", summary: "National arrival hub and wider route connector.", description: "Madrid functions as a major entry point into longer Spain itineraries.", description2: null, description3: null, description4: null, content: "Useful as a starting or ending point for wider custom routes.", mainImage: null, sortOrder: 5, }, ];
for (const city of cities) {
await prisma.city.upsert({
where: { slug: city.slug },
update: city,
create: city,
});
console.log(✅ ${city.name});
}
}
seedCities() .catch((err) => { console.error("❌ seed-cities failed:", err); process.exit(1); }) .finally(async () => { const prisma = getPrisma(); await prisma.$disconnect(); });