5) Frontend – Gallery page (with filters + watermark)

Replace your generator page with this file.

client/src/pages/BusinessAssets/CoverDividerTemplates.tsx

import React, { useMemo, useState } from "react";
import { DashboardTemplatePage } from "@/components/DashboardTemplatePage";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog } from "@/components/ui/dialog";

type CoverTemplate = {
  id: string; title: string; category: string; tags?: string[];
  price_cents: number; currency: string; preview_image_url: string;
  pptx_url?: string; keynote_url?: string; gslides_url?: string; download_bundle_url?: string;
};

const fmt = (c: number, curr = "usd") => `$${(c/100).toFixed(2)}`;

export default function CoverDividerTemplates() {
  const [q, setQ] = useState("");
  const [category, setCategory] = useState<string>("all");
  const [priceMin, setPriceMin] = useState<string>("");
  const [priceMax, setPriceMax] = useState<string>("");

  const { data, isLoading, refetch } = useQuery({
    queryKey: ["cover-templates", q, category, priceMin, priceMax],
    queryFn: async () => {
      const params = new URLSearchParams();
      if (q) params.set("q", q);
      if (category && category !== "all") params.set("category", category);
      if (priceMin) params.set("min", String(Math.max(0, parseInt(priceMin) * 100)));
      if (priceMax) params.set("max", String(Math.max(0, parseInt(priceMax) * 100)));
      const res = await fetch(`/api/cover-templates?${params.toString()}`);
      if (!res.ok) throw new Error("load");
      return res.json() as Promise<CoverTemplate[]>;
    }
  });

  // modal state
  const [open, setOpen] = useState(false);
  const [sel, setSel] = useState<CoverTemplate | null>(null);
  const [custom, setCustom] = useState<string | null>(null);

  function openModal(t: CoverTemplate) { setSel(t); setCustom(null); setOpen(true); }

  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);
    });
    setCustom(dataUrl);
  }

  async function checkout(id: string, customImageUrl?: string) {
    const res = await fetch(`/api/cover-templates/${id}/checkout`, {
      method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include",
      body: JSON.stringify({ customImageUrl })
    });
    if (!res.ok) return toast.error("Checkout failed");
    const j = await res.json();
    window.location.href = j.checkoutUrl;
  }

  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">Curated, editable covers & dividers</h2>
          <p className="text-muted-foreground">Choose a template. Buy as-is or swap one image. Files open in PowerPoint, Keynote, or Google Slides.</p>
        </div>

        {/* Filters */}
        <div className="rounded-lg border p-4 grid grid-cols-1 md:grid-cols-4 gap-3">
          <div className="md:col-span-2">
            <Input placeholder="Search titles…" value={q} onChange={e=>setQ(e.target.value)} />
          </div>
          <Select value={category} onValueChange={setCategory}>
            <SelectTrigger><SelectValue placeholder="Category" /></SelectTrigger>
            <SelectContent>
              <SelectItem value="all">All categories</SelectItem>
              <SelectItem value="business">Business</SelectItem>
              <SelectItem value="minimal">Minimal</SelectItem>
              <SelectItem value="creative">Creative</SelectItem>
              <SelectItem value="corporate">Corporate</SelectItem>
              <SelectItem value="bold">Bold</SelectItem>
              <SelectItem value="tech">Tech</SelectItem>
            </SelectContent>
          </Select>
          <div className="flex gap-2">
            <Input placeholder="Min $" value={priceMin} onChange={e=>setPriceMin(e.target.value)} />
            <Input placeholder="Max $" value={priceMax} onChange={e=>setPriceMax(e.target.value)} />
          </div>
          <div className="md:col-span-4 flex justify-end">
            <Button onClick={()=>refetch()} disabled={isLoading}>Apply</Button>
          </div>
        </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 && openModal(t)} className="text-left rounded-lg border overflow-hidden hover:shadow transition group">
              <div className="relative aspect-[4/3] bg-muted">
                {!isLoading && (
                  <>
                    <img src={t.preview_image_url} alt={t.title} className="w-full h-full object-cover" />
                    {/* Watermark overlay for gallery: */}
                    <div className="absolute inset-0 grid place-items-center pointer-events-none opacity-60 mix-blend-multiply">
                      <div className="wm-bg text-xs sm:text-sm md:text-base">IBrandBiz • Preview</div>
                    </div>
                  </>
                )}
              </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} • {fmt(t.price_cents)}</div>}
              </div>
            </button>
          ))}
        </section>

        {/* Detail / Purchase */}
        <Dialog open={open} onOpenChange={setOpen}>
          {sel && (
            <div className="p-0 md:p-6 max-w-4xl w-full">
              <div className="grid md:grid-cols-[1fr,320px] gap-6">
                <div className="rounded-lg border overflow-hidden relative">
                  <img src={custom || sel.preview_image_url} className="w-full h-full object-cover aspect-[16/9]" />
                  {/* Watermark overlay for non-owners */}
                  <div className="absolute inset-0 grid place-items-center pointer-events-none opacity-60 mix-blend-multiply">
                    <div className="wm-bg text-sm md:text-base">IBrandBiz • Preview</div>
                  </div>
                </div>

                <aside className="space-y-4">
                  <div>
                    <div className="text-lg font-semibold">{sel.title}</div>
                    <div className="text-sm text-muted-foreground">{sel.category}</div>
                    <div className="mt-1 text-xl font-bold">{fmt(sel.price_cents)}</div>
                    <div className="text-xs text-muted-foreground mt-2">Includes: PPTX, Keynote, or Google Slides link (editable).</div>
                  </div>

                  <Button className="w-full" onClick={()=>checkout(sel.id)}>Buy As-Is</Button>

                  <div className="text-xs text-muted-foreground text-center">— 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">
                      {custom ? "Change Image" : "Upload Image to Swap"}
                    </div>
                  </label>
                  <Button className="w-full" variant="secondary" disabled={!custom} onClick={()=>checkout(sel.id, custom!)}>
                    Buy with My Image
                  </Button>

                  <div className="text-xs text-muted-foreground">
                    You’ll receive editable files. Previews are watermarked until purchase.
                  </div>
                </aside>
              </div>
            </div>
          )}
        </Dialog>
      </div>

      {/* Watermark CSS */}
      <style>{`
        .wm-bg {
          font-weight: 700;
          letter-spacing: 1px;
          color: rgba(0,0,0,.25);
          background-image: repeating-linear-gradient(
            45deg,
            rgba(255,255,255,.25) 0 20px,
            rgba(255,255,255,.15) 20px 40px
          );
          padding: .5rem 1rem;
          border-radius: .5rem;
          transform: rotate(-20deg);
          user-select: none;
        }
      `}</style>
    </DashboardTemplatePage>
  );
}


The watermark overlay is client-side CSS (fast). If you want server-side stamped images later, we can add a /api/preview/:id proxy that composites the text.