I’ll ship a full “Gallery + Simple Swap” conversion that replaces that page, plus the backend + schema + storage you’ll need. Copy/paste these pieces and we can move on.

0) What changes (quick)

Replace the current AI generator UI with a gallery of pre-made covers/dividers.

Users can Buy As-Is or Customize (upload one image to swap the hero/photo).

Exports unlocked on purchase (PPTX/PDF/PNG). Free users see watermarked preview.

Minimal schema + routes to back it.

1) DB migrations
1a) SQL (Postgres)
-- 01_cover_templates.sql
CREATE TABLE IF NOT EXISTS cover_templates (
  id            TEXT PRIMARY KEY,
  title         TEXT NOT NULL,
  category      TEXT NOT NULL,          -- e.g., 'business','creative','minimal'
  price_cents   INTEGER NOT NULL DEFAULT 1200,
  preview_url   TEXT NOT NULL,          -- thumbnail
  base_image_url TEXT,                  -- image used by the template (if any)
  download_file TEXT NOT NULL,          -- path or URL to PPTX/PDF/PNG bundle
  is_active     BOOLEAN NOT NULL DEFAULT TRUE,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 02_cover_purchases.sql
CREATE TABLE IF NOT EXISTS cover_purchases (
  id            TEXT PRIMARY KEY,
  user_id       TEXT NOT NULL,
  template_id   TEXT NOT NULL REFERENCES cover_templates(id) ON DELETE CASCADE,
  custom_image_url TEXT,                -- optional user-uploaded replacement
  status        TEXT NOT NULL DEFAULT 'pending', -- pending|paid|delivered|failed
  download_url  TEXT,                   -- final rendered bundle URL
  created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_cover_purchases_user ON cover_purchases(user_id);
CREATE INDEX IF NOT EXISTS idx_cover_templates_active ON cover_templates(is_active);


If you use Drizzle/Knex/Prisma, keep these columns/fields and generate the equivalent migration.

2) Backend: routes + storage (copy in as-is)
2a) Storage helpers

server/storage/coverTemplates.ts

// Adapt to your DB client/ORM (Drizzle/Knex/Prisma). These are generic examples.

export type CoverTemplate = {
  id: string; title: string; category: string; price_cents: number;
  preview_url: string; base_image_url?: string | null; download_file: string;
  is_active: boolean; created_at: string;
};

export type CoverPurchase = {
  id: string; user_id: string; template_id: string; custom_image_url?: string | null;
  status: 'pending'|'paid'|'delivered'|'failed';
  download_url?: string | null; created_at: string;
};

export async function listActiveCoverTemplates(): Promise<CoverTemplate[]> {
  // SELECT * FROM cover_templates WHERE is_active = true ORDER BY created_at DESC
  // TODO: replace with your DB call
  return db('cover_templates').where({ is_active: true }).orderBy('created_at', 'desc');
}

export async function getCoverTemplate(id: string): Promise<CoverTemplate | null> {
  const row = await db('cover_templates').where({ id }).first();
  return row || null;
}

export async function createPurchase(userId: string, templateId: string, customImageUrl?: string) {
  const id = crypto.randomUUID();
  const rec = {
    id, user_id: userId, template_id: templateId, custom_image_url: customImageUrl || null,
    status: 'pending', download_url: null
  };
  await db('cover_purchases').insert(rec);
  return rec;
}

export async function setPurchaseStatus(id: string, status: CoverPurchase['status'], downloadUrl?: string) {
  await db('cover_purchases').where({ id }).update({
    status, download_url: downloadUrl ?? null
  });
}

export async function getUserPurchases(userId: string) {
  return db('cover_purchases').where({ user_id: userId }).orderBy('created_at', 'desc');
}

2b) Routes

server/routes.coverTemplates.ts

import express from 'express';
import {
  listActiveCoverTemplates, getCoverTemplate,
  createPurchase, setPurchaseStatus
} from '../storage/coverTemplates';

const router = express.Router();

// Swap these with your real middlewares
const authenticate = (req: any, res: any, next: any) => { if (!req.user) return res.status(401).json({error:'auth'}); next(); };
const requireProOrPaid = (req: any, res: any, next: any) => next();

// GET list for gallery
router.get('/api/cover-templates', async (_req, res) => {
  const rows = await listActiveCoverTemplates();
  res.json(rows);
});

// GET single (details)
router.get('/api/cover-templates/:id', async (req, res) => {
  const tpl = await getCoverTemplate(req.params.id);
  if (!tpl) return res.status(404).json({ error: 'Not found' });
  res.json(tpl);
});

// POST /buy-as-is (creates purchase and returns a Stripe Checkout URL OR demo success)
router.post('/api/cover-templates/:id/buy', authenticate, async (req: any, res) => {
  const tpl = await getCoverTemplate(req.params.id);
  if (!tpl) return res.status(404).json({ error: 'Not found' });

  const purchase = await createPurchase(req.user.id, tpl.id);
  // TODO: integrate Stripe Checkout; for MVP we mark paid and return download
  await setPurchaseStatus(purchase.id, 'paid', tpl.download_file);
  return res.json({ success: true, purchaseId: purchase.id, downloadUrl: tpl.download_file });
});

// POST /customize (image dataUrl or uploaded URL)
router.post('/api/cover-templates/:id/customize', authenticate, async (req: any, res) => {
  const tpl = await getCoverTemplate(req.params.id);
  if (!tpl) return res.status(404).json({ error: 'Not found' });

  const { imageDataUrl, imageUrl } = req.body || {};
  if (!imageDataUrl && !imageUrl) return res.status(400).json({ error: 'Missing image' });

  // In a full system you would render the template with the new image via headless renderer.
  // MVP: store the custom image and return same template bundle (or pre-render variant).
  const purchase = await createPurchase(req.user.id, tpl.id, imageUrl || imageDataUrl);
  await setPurchaseStatus(purchase.id, 'paid', tpl.download_file);

  res.json({
    success: true,
    purchaseId: purchase.id,
    previewUrl: imageUrl || imageDataUrl, // client can show this
    downloadUrl: tpl.download_file
  });
});

// (Optional) get my purchases
router.get('/api/me/cover-purchases', authenticate, async (req: any, res) => {
  const rows = await getUserPurchases(req.user.id);
  res.json(rows);
});

export default router;


Wire it in main server file:

import coverTemplatesRouter from './routes.coverTemplates';
app.use(coverTemplatesRouter);

3) Frontend: replace the page with Gallery + Detail + Simple Swap

Replace your current file entirely with this.
Path: client/src/pages/BusinessAssets/CoverDividerTemplates.tsx
(If your path is different, keep the export default name.)

import React, { useMemo, useState } from "react";
import { DashboardTemplatePage } from '@/components/DashboardTemplatePage';
import { useQuery, useMutation } from "@tanstack/react-query";
import { Dialog } from "@/components/ui/dialog"; // or your modal component
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";

type CoverTemplate = {
  id: string;
  title: string;
  category: string;
  price_cents: number;
  preview_url: string;
  base_image_url?: string | null;
  download_file: string;
};

function price(p: number) { return `$${(p / 100).toFixed(2)}`; }

export default function CoverDividerTemplates() {
  const { data, isLoading } = useQuery({
    queryKey: ["cover-templates"],
    queryFn: async () => {
      const res = await fetch("/api/cover-templates");
      if (!res.ok) throw new Error("Failed");
      return res.json() as Promise<CoverTemplate[]>;
    }
  });

  const [open, setOpen] = useState(false);
  const [selected, setSelected] = useState<CoverTemplate | null>(null);
  const [customImage, setCustomImage] = useState<string | null>(null);

  function openDetail(t: CoverTemplate) {
    setSelected(t);
    setCustomImage(null);
    setOpen(true);
  }

  const buyAsIs = useMutation({
    mutationFn: async (id: string) => {
      const r = await fetch(`/api/cover-templates/${id}/buy`, { method: "POST", credentials: "include" });
      if (!r.ok) throw new Error("Buy failed");
      return r.json() as Promise<{ downloadUrl: string }>;
    },
    onSuccess: (d) => {
      toast.success("Purchase complete");
      window.open(d.downloadUrl, "_blank");
    },
    onError: () => toast.error("Could not complete purchase")
  });

  const customize = useMutation({
    mutationFn: async ({ id, imageDataUrl }: { id: string; imageDataUrl: string }) => {
      const r = await fetch(`/api/cover-templates/${id}/customize`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({ imageDataUrl })
      });
      if (!r.ok) throw new Error("Customize failed");
      return r.json() as Promise<{ previewUrl: string; downloadUrl: string }>;
    },
    onSuccess: (d) => {
      toast.success("Customized and purchased");
      window.open(d.downloadUrl, "_blank");
    },
    onError: () => toast.error("Could not customize")
  });

  async function onPickImage(e: React.ChangeEvent<HTMLInputElement>) {
    const f = e.target.files?.[0];
    if (!f) return;
    const dataUrl = await new Promise<string>((resolve, reject) => {
      const r = new FileReader();
      r.onload = () => resolve(r.result as string);
      r.onerror = reject;
      r.readAsDataURL(f);
    });
    setCustomImage(dataUrl);
  }

  return (
    <DashboardTemplatePage title="Cover & Divider Templates">
      <div className="space-y-6">
        <div className="bg-card p-6 rounded-lg border">
          <h2 className="text-xl font-semibold mb-1">Choose a professional cover/divider</h2>
          <p className="text-muted-foreground">Pick from curated templates. Buy as-is or swap a single image.</p>
        </div>

        {/* Gallery */}
        <section className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-6">
          {(isLoading ? Array.from({length:8},(_,i)=>({id:`s${i}`} as any)) : (data||[])).map((t: CoverTemplate) => (
            <button
              key={t.id}
              onClick={() => !isLoading && openDetail(t)}
              className="text-left rounded-lg border overflow-hidden hover:shadow transition"
            >
              <div className="aspect-[4/3] bg-muted">
                {!isLoading && <img src={t.preview_url} alt={t.title} className="w-full h-full object-cover" />}
              </div>
              <div className="p-3">
                <div className="font-medium truncate">{isLoading ? 'Loading…' : t.title}</div>
                {!isLoading && <div className="text-xs text-muted-foreground">{t.category} • {price(t.price_cents)}</div>}
              </div>
            </button>
          ))}
        </section>

        {/* Detail / Customizer */}
        <Dialog open={open} onOpenChange={setOpen}>
          {selected && (
            <div className="p-0 md:p-6 max-w-4xl w-full">
              <div className="grid md:grid-cols-[1fr,320px] gap-6">
                {/* Preview */}
                <div className="rounded-lg border overflow-hidden">
                  <div className="aspect-[16/9] bg-muted relative">
                    {/* simple swap: overlay custom image if provided */}
                    <img
                      src={customImage || selected.preview_url}
                      alt={selected.title}
                      className="absolute inset-0 w-full h-full object-cover"
                    />
                    {/* Optional overlay UI like title bars could be baked into preview_url */}
                  </div>
                </div>

                {/* Actions */}
                <aside className="space-y-4">
                  <div>
                    <div className="text-lg font-semibold">{selected.title}</div>
                    <div className="text-sm text-muted-foreground">{selected.category}</div>
                    <div className="mt-1 text-xl font-bold">{price(selected.price_cents)}</div>
                  </div>

                  <div className="space-y-2">
                    <Button className="w-full" onClick={() => buyAsIs.mutate(selected.id)} disabled={buyAsIs.isLoading}>
                      Buy As-Is
                    </Button>

                    <div className="text-xs text-muted-foreground pt-2">— or —</div>

                    <label className="w-full">
                      <input type="file" accept="image/*" className="hidden" onChange={onPickImage} />
                      <div className="w-full border rounded-lg p-3 text-center cursor-pointer hover:bg-accent">
                        {customImage ? "Change Image" : "Upload Image to Swap"}
                      </div>
                    </label>

                    <Button
                      className="w-full"
                      variant="secondary"
                      onClick={() => customImage && customize.mutate({ id: selected.id, imageDataUrl: customImage })}
                      disabled={!customImage || customize.isLoading}
                    >
                      {customize.isLoading ? "Processing…" : "Buy with My Image"}
                    </Button>
                  </div>

                  <div className="text-xs text-muted-foreground">
                    Free users see watermarked preview; Pro & purchases unlock full-resolution PPTX/PDF/PNG.
                  </div>
                </aside>
              </div>
            </div>
          )}
        </Dialog>
      </div>
    </DashboardTemplatePage>
  );
}


Notes

The preview simply swaps preview_url with customImage. Your real export/bundle can remain the same file (download_file) for MVP; later you can render a variant with the swapped image.

If you already use a Dialog component, keep your import; otherwise, replace with your modal.

4) Seed a few templates (optional helper script)
// server/scripts/seedCoverTemplates.ts
import { db } from '../db';
async function run() {
  const rows = [
    {
      id: 'cov-minimal-band',
      title: 'Minimal Modern (Accent Band)',
      category: 'business',
      price_cents: 1200,
      preview_url: 'https://your-cdn/covers/minimal-band.jpg',
      base_image_url: 'https://your-cdn/covers/minimal-band-base.jpg',
      download_file: 'https://your-cdn/covers/minimal-band-bundle.zip',
      is_active: true
    },
    {
      id: 'cov-center-hero',
      title: 'Center Hero',
      category: 'creative',
      price_cents: 1200,
      preview_url: 'https://your-cdn/covers/center-hero.jpg',
      base_image_url: 'https://your-cdn/covers/center-hero-base.jpg',
      download_file: 'https://your-cdn/covers/center-hero-bundle.zip',
      is_active: true
    }
  ];
  for (const r of rows) {
    await db('cover_templates').insert(r).onConflict('id').ignore();
  }
  console.log('Seeded cover templates.');
}
run().catch(console.error);

5) Sidebar label (no code needed)

Keep the same menu location (Business Assets → Templates → Cover & Divider Templates). The new page you pasted above replaces the old generator screen.

6) QA checklist (for this change)

Gallery loads with seeded templates.

Clicking a card opens detail modal.

Buy As-Is returns a download (MVP: same bundle).

Uploading an image updates the preview; Buy with My Image returns a download.

Watermark/Pro gating: show watermark overlay in preview for free users; allow full download for Pro/purchased (you can gate buttons in UI if !isPro).