← back to Trademarks Copyright

src/app/alerts/page.tsx

163 lines

import Link from "next/link";
import { query } from "@/lib/db";

export const metadata = {
  title: "Alerts — Ranked Catalog",
  description: "Every abandoned trademark + public-domain work in the catalog, ranked by composite score — the same content that gets sent to steve-office via George.",
};

export const dynamic = "force-dynamic";

type Idea = { title: string; description: string };
type Row = {
  id: number; kind: "trademark" | "copyright"; name: string; status: string;
  original_owner: string | null; nice_class: string | null; goods_services: string | null;
  expired_date: string; registered_date: string | null; registration_number: string | null;
  source: string; source_url: string | null; notes: string | null;
  distinctiveness_score: number; recognition_score: number;
  domain_available: boolean | null; competing_active_marks: number; monetization_breadth: number;
  composite_score: string;
  monetization_ideas: Idea[];
};

export default async function AlertsPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {
  const sp = await searchParams;
  const kind = sp.kind || "";
  const where: string[] = ["composite_score IS NOT NULL"];
  const params: unknown[] = [];
  if (kind === "trademark" || kind === "copyright") {
    params.push(kind);
    where.push(`kind = $${params.length}`);
  }

  const { rows } = await query<Row>(
    `SELECT id, kind, name, status, original_owner, nice_class, goods_services,
            expired_date, registered_date, registration_number,
            source, source_url, notes,
            distinctiveness_score, recognition_score,
            domain_available, competing_active_marks, monetization_breadth,
            composite_score, monetization_ideas
     FROM items
     WHERE ${where.join(" AND ")}
     ORDER BY composite_score DESC`,
    params
  );

  const total = rows.length;

  return (
    <article className="mx-auto max-w-4xl">
      <div className="card p-6 mb-4">
        <div className="flex flex-wrap items-center gap-3">
          <div>
            <div className="text-xs uppercase tracking-widest text-brass">Ranked Catalog</div>
            <h1 className="mt-2 text-3xl font-semibold">All {total} alerts, scored 0–100.</h1>
            <p className="mt-1 text-sm text-ink/70">
              The same items George sent to <code>steve@designerwallcoverings.com</code> today — ranked, cross-referenced, and with full metadata.
            </p>
          </div>
          <div className="ml-auto flex items-center gap-2 text-sm">
            <Link href="/alerts" className={`tag ${!kind ? "bg-ink/10 font-bold" : ""}`}>All</Link>
            <Link href="/alerts?kind=trademark" className={`tag ${kind === "trademark" ? "bg-ink/10 font-bold" : ""}`}>Trademarks</Link>
            <Link href="/alerts?kind=copyright" className={`tag ${kind === "copyright" ? "bg-ink/10 font-bold" : ""}`}>Public domain</Link>
          </div>
        </div>
      </div>

      <ol className="space-y-3" start={1}>
        {rows.map((r, idx) => (
          <AlertCard key={r.id} rank={idx + 1} item={r} />
        ))}
      </ol>

      <div className="mt-8 card p-6 text-sm text-ink/70">
        This catalog powers the daily 6 AM alert. Rank and score are recomputed whenever seed data or the scoring weights change.
      </div>
    </article>
  );
}

function medal(rank: number): string {
  return rank === 1 ? "🥇" : rank === 2 ? "🥈" : rank === 3 ? "🥉" : `#${rank}`;
}

function scoreColor(n: number): string {
  const hue = Math.round((n / 100) * 120);
  return `hsl(${hue} 55% 40%)`;
}

function AlertCard({ rank, item }: { rank: number; item: Row }) {
  const score = Number(item.composite_score);
  const date = new Date(item.expired_date).toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
  const ideas: Idea[] = Array.isArray(item.monetization_ideas) ? item.monetization_ideas : [];
  return (
    <li className="card overflow-hidden" id={`rank-${rank}`}>
      <div className="flex items-start gap-4 p-5 border-l-4" style={{ borderColor: "#b38f4e" }}>
        <div className="shrink-0 text-center" style={{ minWidth: 72 }}>
          <div className="text-3xl">{medal(rank)}</div>
          <div className="mt-1 inline-block rounded px-2 py-0.5 text-xs font-bold text-white" style={{ backgroundColor: scoreColor(score) }}>
            {score.toFixed(1)}
          </div>
        </div>
        <div className="flex-1">
          <div className="flex flex-wrap items-baseline gap-2">
            <h2 className="text-xl font-semibold">
              <Link href={`/item/${item.id}`} className="hover:text-brass">{item.name}</Link>
            </h2>
            <span className="tag">{item.kind}</span>
            <span className="tag">{item.status}</span>
            <span className="text-xs text-ink/60">expired {date}</span>
            {item.registration_number && <span className="text-xs text-ink/60">· reg #{item.registration_number}</span>}
          </div>

          <dl className="mt-3 grid grid-cols-1 gap-x-6 gap-y-1 text-sm md:grid-cols-2">
            <KV k="Original owner" v={item.original_owner} />
            {item.nice_class && <KV k="NICE class" v={item.nice_class} />}
            {item.goods_services && <KV k="Goods / services" v={item.goods_services} />}
            <KV k="Distinctiveness" v={String(item.distinctiveness_score)} />
            <KV k="Recognition" v={String(item.recognition_score)} />
            <KV k="Domain (.com)" v={item.domain_available === true ? "yes" : item.domain_available === false ? "no" : "unknown"} />
            <KV k="Competing live marks" v={String(item.competing_active_marks)} />
            <KV k="Monetization breadth" v={String(item.monetization_breadth)} />
            <KV k="Source" v={item.source_url
              ? <a href={item.source_url} target="_blank" rel="noreferrer" className="text-brass hover:underline">{item.source}</a>
              : item.source
            } />
          </dl>

          {item.notes && (
            <p className="mt-3 text-sm text-ink/80"><strong>Notes:</strong> {item.notes}</p>
          )}

          {ideas.length > 0 && (
            <div className="mt-3">
              <div className="text-xs uppercase tracking-wide text-brass">Monetization angles</div>
              <ul className="mt-1 space-y-1 text-sm">
                {ideas.map((idea, i) => (
                  <li key={i}>
                    <strong>{idea.title}</strong> — <span className="text-ink/80">{idea.description}</span>
                  </li>
                ))}
              </ul>
            </div>
          )}

          <div className="mt-3 text-xs">
            <Link href={`/item/${item.id}`} className="text-brass hover:underline">Open in app →</Link>
            <a href={`#rank-${rank}`} className="ml-3 text-ink/40 hover:text-brass">#</a>
          </div>
        </div>
      </div>
    </li>
  );
}

function KV({ k, v }: { k: string; v: React.ReactNode }) {
  return (
    <>
      <dt className="text-ink/55">{k}</dt>
      <dd className="text-ink/90">{v ?? "—"}</dd>
    </>
  );
}