← back to Grant

components/landing/FitFeed.tsx

202 lines

'use client';

import { useMemo, useState } from 'react';
import Link from 'next/link';

/* The four funding sources Grant unifies. Mirrors the real grants.source_api /
   funder-type split — federal (Grants.gov + Federal Register), foundation,
   corporate, and state/local. */
type Source = 'fed' | 'found' | 'corp' | 'local';

interface FeedItem {
  /** mirrors grants.title */
  title: string;
  /** mirrors grants.funder */
  funder: string;
  source: Source;
  /** mirrors grants.ai_fit_score (0–100) */
  fit: number;
  /** rendered from grants.amount_min / amount_max */
  amount: string;
  /** rendered from grants.deadline */
  due: string;
  /** low-fit row, pushed down by ranking — illustrates the filter */
  lowFit?: boolean;
}

const SOURCE_LABEL: Record<Source, string> = {
  fed: 'Federal',
  found: 'Foundation',
  corp: 'Corporate',
  local: 'State / Local',
};

/* Ring tint per source, matching the badge colors. */
const SCORE_TINT: Record<Source, { bg: string; color: string }> = {
  fed: { bg: '#e4f5ee', color: '#047857' },
  found: { bg: '#ede9fe', color: '#7c3aed' },
  corp: { bg: '#ffedd5', color: '#ea580c' },
  local: { bg: '#cffafe', color: '#0891b2' },
};

const LOW_TINT = { bg: '#eef3f0', color: '#5a6b62' };

/* Representative preview feed — real, public grant programs shown as an example
   of what a unified, fit-scored stream looks like. The signed-in app replaces
   this with the org's live /api/grants feed scored against its mission. */
const FEED: FeedItem[] = [
  { title: 'NEA Grants for Arts Projects', funder: 'National Endowment for the Arts', source: 'fed', fit: 94, amount: '$10k–$100k', due: 'Due in 6 days' },
  { title: 'Kresge Foundation — Health Equity', funder: 'The Kresge Foundation', source: 'found', fit: 90, amount: 'up to $250k', due: 'Due in 19 days' },
  { title: 'CA Arts Council — Local Impact', funder: 'California Arts Council', source: 'local', fit: 88, amount: '$18k', due: 'Due in 31 days' },
  { title: 'Bank of America Charitable Fund', funder: 'Bank of America', source: 'corp', fit: 81, amount: '$25k', due: 'Due in 44 days' },
  { title: 'Mellon Foundation — Humanities in Place', funder: 'Andrew W. Mellon Foundation', source: 'found', fit: 76, amount: 'up to $500k', due: 'Due in 52 days' },
  { title: 'Rural Broadband Infrastructure Pilot', funder: 'USDA Rural Development', source: 'fed', fit: 38, amount: '$1M+', due: 'Low fit — filtered down', lowFit: true },
];

const FILTERS: { key: 'all' | Source; label: string }[] = [
  { key: 'all', label: 'All' },
  { key: 'fed', label: 'Federal' },
  { key: 'found', label: 'Foundation' },
  { key: 'corp', label: 'Corporate' },
  { key: 'local', label: 'Local' },
];

function FeedCard({ item }: { item: FeedItem }) {
  const tint = item.lowFit ? LOW_TINT : SCORE_TINT[item.source];
  return (
    <div className="gcard">
      <div className="score" style={{ background: tint.bg, color: tint.color }}>
        <span className="pct">{item.fit}%</span>
        <span className="lab">fit</span>
      </div>
      <div className="gmeta">
        <div className="nm">{item.title}</div>
        <div className="row">
          <span className={`src ${item.source}`}>{SOURCE_LABEL[item.source]}</span>
          <span>{item.amount}</span>
          <span className={item.lowFit ? 'due low' : 'due'}>{item.due}</span>
        </div>
      </div>
    </div>
  );
}

export default function FitFeed() {
  const [filter, setFilter] = useState<'all' | Source>('all');

  const visible = useMemo(
    () => (filter === 'all' ? FEED : FEED.filter((g) => g.source === filter)),
    [filter],
  );

  return (
    <div className="ff">
      {/* Nav */}
      <nav className="nav">
        <div className="nbar">
          <div className="logo"><span className="lbadge">G</span>Grant</div>
          <div className="navlinks">
            <a href="#feed">Feed</a>
            <a href="#why">Fit scores</a>
            <a href="#why">Proposals</a>
            <Link href="/login">Sign in</Link>
            <Link className="btn" href="/login">Start free</Link>
          </div>
        </div>
      </nav>

      {/* Hero + live feed */}
      <section className="wrap hero" id="feed">
        <div>
          <span className="pill"><span className="dot" />One feed, every funding source</span>
          <h1>Stop tab-hopping. <span>Every grant, ranked by fit.</span></h1>
          <p className="sub">
            Federal, foundation, corporate, and local grants — siloed everywhere
            else — unified into a single feed and scored against your mission.
            See the 94%-fit opportunity before the 40% noise.
          </p>
          <div className="cta-row">
            <Link className="btn btn-lg" href="/login">See your fit scores</Link>
            <a className="btn-out" href="#feed">Browse the feed</a>
          </div>
          <p className="undr">
            Rivals are siloed — Grants.gov is federal-only, Candid is
            foundation-centric. Grant scores them all together.
          </p>
        </div>

        <div className="feed">
          <div className="feedhead">
            <div className="t">Your fit-scored feed</div>
            <div className="filters" role="tablist" aria-label="Filter feed by source">
              {FILTERS.map((f) => (
                <button
                  key={f.key}
                  role="tab"
                  aria-selected={filter === f.key}
                  className={filter === f.key ? 'chip on' : 'chip'}
                  onClick={() => setFilter(f.key)}
                >
                  {f.label}
                </button>
              ))}
            </div>
          </div>
          <div className="feedlist">
            {visible.length === 0 ? (
              <div className="feedempty">No {filter} opportunities in this sample feed.</div>
            ) : (
              visible.map((item) => <FeedCard key={item.title} item={item} />)
            )}
          </div>
        </div>
      </section>

      {/* Stat strip */}
      <div className="strip">
        <div className="wrap">
          <div className="scell"><b>4 source types</b><span>federal · foundation · corporate · local</span></div>
          <div className="scell"><b>1 fit score</b><span>tuned to your org profile</span></div>
          <div className="scell"><b>No double-tooling</b><span>free federal feed built in</span></div>
          <div className="scell"><b>Individuals welcome</b><span>not just funded dev teams</span></div>
        </div>
      </div>

      {/* Why */}
      <section className="why wrap" id="why">
        <h2>Precision the firehose can&rsquo;t give you</h2>
        <p className="l">GrantWatch lists everything. Candid has the data. Neither ranks it for <em>you</em>.</p>
        <div className="cols">
          <div className="col">
            <span className="k">Source-unified</span>
            <h3>One ranked stream</h3>
            <p>Federal + foundation + corporate + local + individual in one ranked stream — no more paying for one tool and still tabbing over to Grants.gov.</p>
          </div>
          <div className="col">
            <span className="k">Fit scoring</span>
            <h3>Ranked, not listed</h3>
            <p>Every opportunity scored against your mission, budget, and history. The 38%-fit gets pushed down; the 94% rises to the top.</p>
          </div>
          <div className="col">
            <span className="k">Action-first</span>
            <h3>Discovery into draft</h3>
            <p>Click a high-fit grant and draft the LOI in the same screen. Discovery flows straight into a proposal and outreach.</p>
          </div>
        </div>
      </section>

      {/* Closer */}
      <section className="closer">
        <div className="wrap">
          <h2>See what actually fits your mission.</h2>
          <Link className="btn-w" href="/login">See your fit scores</Link>
        </div>
      </section>

      <footer className="foot">
        Grant — Deadline River · One fit-scored feed for under-resourced nonprofits · Federal data via the U.S. Federal Register API
      </footer>
    </div>
  );
}