Perfect, babe. Let’s lock in a clean, tabbed Profile page tied to the bottom-left user card — plus the minimal backend you need for edits, password change, avatar upload (stub), and Stripe billing portal.

1) UX wiring

Bottom-left user card → /profile

// wherever you render the bottom-left user card
<a href="/profile" className="flex items-center gap-2 px-3 py-2 rounded-xl hover:bg-gray-100">
  <img src={me.avatarUrl || "/avatar-default.png"} className="w-8 h-8 rounded-full" />
  <div className="text-sm">
    <div className="font-semibold">{me.name || me.email}</div>
    <div className="text-gray-500">{me.email}</div>
  </div>
</a>

2) Profile page (tabs)

src/pages/Profile/ProfilePage.tsx

import { useEffect, useState } from "react";
import { fetchMe } from "@/store/auth";
import { ProfileInfoTab } from "./tabs/ProfileInfoTab";
import { SubscriptionTab } from "./tabs/SubscriptionTab";
import { PreferencesTab } from "./tabs/PreferencesTab";
import { SecurityTab } from "./tabs/SecurityTab";

type Me = {
  authenticated: boolean;
  email: string;
  name?: string;
  company?: string;
  avatarUrl?: string;
  isPaid?: boolean;
  proActivatedAt?: string | null;
  proWelcomeDismissed?: boolean;
};

export default function ProfilePage() {
  const [me, setMe] = useState<Me | null>(null);
  const [tab, setTab] = useState<"profile"|"subscription"|"preferences"|"security">("profile");

  useEffect(() => { fetchMe().then(setMe as any); }, []);
  if (!me) return <div className="p-8">Loading…</div>;
  if (!me.authenticated) return <div className="p-8">Please sign in.</div>;

  return (
    <div className="max-w-5xl mx-auto py-8 grid grid-cols-12 gap-6">
      {/* Left column: card */}
      <aside className="col-span-12 md:col-span-4">
        <div className="rounded-2xl border p-6">
          <div className="flex items-center gap-3 mb-4">
            <img className="w-16 h-16 rounded-full" src={me.avatarUrl || "/avatar-default.png"} />
            <div>
              <div className="font-semibold">{me.name || "Your Name"}</div>
              <div className="text-sm text-gray-500">{me.email}</div>
            </div>
          </div>
          <div className="grid grid-cols-2 gap-2 text-sm">
            <div className="rounded-lg border p-3">
              <div className="text-gray-500">Plan</div>
              <div className="font-semibold">{me.isPaid ? "Pro" : "Free"}</div>
            </div>
            <div className="rounded-lg border p-3">
              <div className="text-gray-500">Member since</div>
              <div className="font-semibold">{me.proActivatedAt ? new Date(me.proActivatedAt).toLocaleDateString() : "—"}</div>
            </div>
          </div>
        </div>
        {/* Tabs */}
        <div className="mt-4 rounded-2xl border p-2">
          {[
            ["profile","Profile Info"],
            ["subscription","Subscription"],
            ["preferences","Preferences"],
            ["security","Security"],
          ].map(([k,label]) => (
            <button
              key={k}
              onClick={()=>setTab(k as any)}
              className={`w-full text-left px-3 py-2 rounded-xl text-sm ${
                tab===k ? "bg-black text-white" : "hover:bg-gray-100"
              }`}
            >{label}</button>
          ))}
        </div>
      </aside>

      {/* Right column: tab content */}
      <main className="col-span-12 md:col-span-8">
        {tab==="profile" && <ProfileInfoTab me={me} onUpdated={setMe as any} />}
        {tab==="subscription" && <SubscriptionTab me={me} />}
        {tab==="preferences" && <PreferencesTab me={me} onUpdated={setMe as any} />}
        {tab==="security" && <SecurityTab />}
      </main>
    </div>
  );
}

Tabs (lean MVP)

src/pages/Profile/tabs/ProfileInfoTab.tsx

export function ProfileInfoTab({ me, onUpdated }: any){
  const [name, setName] = useState(me.name || "");
  const [company, setCompany] = useState(me.company || "");
  const [saving, setSaving] = useState(false);

  const save = async () => {
    setSaving(true);
    const res = await fetch("/api/profile/update", {
      method: "POST", headers: {"Content-Type":"application/json"}, credentials: "include",
      body: JSON.stringify({ name, company }),
    });
    const out = await res.json();
    setSaving(false);
    if (res.ok) onUpdated((prev:any)=>({ ...prev, name: out.name, company: out.company }));
    else alert(out.error || "Failed to save");
  };

  const onAvatar = async (e:any) => {
    const f = e.target.files?.[0]; if (!f) return;
    const form = new FormData(); form.append("file", f);
    const res = await fetch("/api/profile/avatar", { method:"POST", credentials:"include", body: form });
    const out = await res.json();
    if (res.ok) onUpdated((prev:any)=>({ ...prev, avatarUrl: out.url })); else alert(out.error || "Upload failed");
  };

  return (
    <div className="rounded-2xl border p-6">
      <h2 className="text-xl font-semibold mb-4">Profile Info</h2>
      <label className="block text-sm mb-1">Avatar</label>
      <div className="flex items-center gap-3 mb-4">
        <img src={me.avatarUrl || "/avatar-default.png"} className="w-14 h-14 rounded-full" />
        <input type="file" accept="image/*" onChange={onAvatar} />
      </div>

      <label className="block text-sm mb-1">Full name</label>
      <input className="w-full border rounded-xl px-3 py-2 mb-3" value={name} onChange={e=>setName(e.target.value)} />

      <label className="block text-sm mb-1">Company / Brand</label>
      <input className="w-full border rounded-xl px-3 py-2 mb-6" value={company} onChange={e=>setCompany(e.target.value)} />

      <button className="rounded-xl bg-black text-white px-4 py-2" onClick={save} disabled={saving}>
        {saving ? "Saving…" : "Save changes"}
      </button>
    </div>
  );
}


src/pages/Profile/tabs/SubscriptionTab.tsx

export function SubscriptionTab({ me }: any){
  const openPortal = async () => {
    const res = await fetch("/api/pay/portal", { method:"POST", credentials:"include" });
    const out = await res.json();
    if (res.ok && out.url) window.location.href = out.url;
    else alert(out.error || "Portal unavailable");
  };

  return (
    <div className="rounded-2xl border p-6">
      <h2 className="text-xl font-semibold mb-2">Subscription</h2>
      <div className="text-sm text-gray-600 mb-6">
        Current plan: <span className="font-semibold">{me.isPaid ? "Pro" : "Free"}</span>
      </div>

      {me.isPaid ? (
        <button className="rounded-xl bg-black text-white px-4 py-2" onClick={openPortal}>
          Manage billing
        </button>
      ) : (
        <button className="rounded-xl bg-black text-white px-4 py-2" onClick={() => openPaywallModal({source:"profile_sub"})}>
          Upgrade to Pro
        </button>
      )}
    </div>
  );
}


src/pages/Profile/tabs/PreferencesTab.tsx

export function PreferencesTab({ me, onUpdated }: any){
  const [emailNews, setEmailNews] = useState(true);
  const save = async () => {
    await fetch("/api/profile/preferences", {
      method:"POST", headers:{"Content-Type":"application/json"}, credentials:"include",
      body: JSON.stringify({ emailNews }),
    });
  };
  return (
    <div className="rounded-2xl border p-6">
      <h2 className="text-xl font-semibold mb-4">Preferences</h2>
      <label className="flex items-center gap-3">
        <input type="checkbox" checked={emailNews} onChange={e=>setEmailNews(e.target.checked)} />
        Receive product updates by email
      </label>
      <button className="mt-4 rounded-xl bg-black text-white px-4 py-2" onClick={save}>Save preferences</button>
    </div>
  );
}


src/pages/Profile/tabs/SecurityTab.tsx

export function SecurityTab(){
  const [current, setCurrent] = useState("");
  const [next, setNext] = useState("");
  const [confirm, setConfirm] = useState("");

  const change = async () => {
    if (next !== confirm) return alert("Passwords do not match");
    const res = await fetch("/api/profile/change-password", {
      method:"POST", headers:{"Content-Type":"application/json"}, credentials:"include",
      body: JSON.stringify({ current, next }),
    });
    const out = await res.json();
    res.ok ? alert("Password updated") : alert(out.error || "Failed");
    setCurrent(""); setNext(""); setConfirm("");
  };

  return (
    <div className="rounded-2xl border p-6">
      <h2 className="text-xl font-semibold mb-4">Security</h2>
      <label className="block text-sm mb-1">Current password</label>
      <input className="w-full border rounded-xl px-3 py-2 mb-3" type="password" value={current} onChange={e=>setCurrent(e.target.value)} />
      <label className="block text-sm mb-1">New password</label>
      <input className="w-full border rounded-xl px-3 py-2 mb-3" type="password" value={next} onChange={e=>setNext(e.target.value)} />
      <label className="block text-sm mb-1">Confirm new password</label>
      <input className="w-full border rounded-xl px-3 py-2 mb-6" type="password" value={confirm} onChange={e=>setConfirm(e.target.value)} />
      <button className="rounded-xl bg-black text-white px-4 py-2" onClick={change}>Update password</button>
    </div>
  );
}

3) Minimal backend endpoints
Update profile info
// server/routes/profile.ts
import { Router } from "express";
import multer from "multer";
import bcrypt from "bcrypt";
import { requireAuth, AuthedRequest } from "../auth/middleware";
import { db, Users } from "../db";

const router = Router();
const upload = multer({ dest: "uploads/" }); // swap to S3 later

router.post("/api/profile/update", requireAuth, (req: AuthedRequest, res) => {
  const { name, company } = req.body || {};
  db.prepare("ALTER TABLE users ADD COLUMN name TEXT").run(); // safe if exists? wrap in try
  db.prepare("ALTER TABLE users ADD COLUMN company TEXT").run();
  db.prepare("UPDATE users SET name=?, company=? WHERE id=?").run(name ?? null, company ?? null, req.user!.id);
  res.json({ name, company });
});

router.post("/api/profile/avatar", requireAuth, upload.single("file"), (req: AuthedRequest, res) => {
  // store file path, or upload to S3 and store URL
  const url = `/uploads/${req.file?.filename}`; // dev-only
  db.prepare("ALTER TABLE users ADD COLUMN avatar_url TEXT").run();
  db.prepare("UPDATE users SET avatar_url=? WHERE id=?").run(url, req.user!.id);
  res.json({ url });
});

router.post("/api/profile/preferences", requireAuth, (req: AuthedRequest, res) => {
  db.exec(`CREATE TABLE IF NOT EXISTS user_prefs (user_id INT PRIMARY KEY, email_news INT DEFAULT 1)`);
  const { emailNews } = req.body || {};
  db.prepare("INSERT INTO user_prefs (user_id, email_news) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET email_news=excluded.email_news")
    .run(req.user!.id, emailNews ? 1 : 0);
  res.json({ ok: true });
});

router.post("/api/profile/change-password", requireAuth, async (req: AuthedRequest, res) => {
  const { current, next } = req.body || {};
  const row = db.prepare("SELECT password_hash FROM users WHERE id=?").get(req.user!.id) as any;
  const ok = await bcrypt.compare(current || "", row.password_hash);
  if (!ok) return res.status(400).json({ error: "Current password incorrect" });
  const nextHash = await bcrypt.hash(next || "", 10);
  db.prepare("UPDATE users SET password_hash=? WHERE id=?").run(nextHash, req.user!.id);
  res.json({ ok: true });
});

export default router;


Mount it:

// server/index.ts
import profileRoutes from "./routes/profile";
app.use(profileRoutes);

Stripe Billing Portal
// server/pay/stripe.ts (add endpoint)
router.post("/api/pay/portal", requireAuth, async (req: AuthedRequest, res) => {
  try {
    const sub = Subs.findByUser(req.user!.id);
    const session = await stripe.billingPortal.sessions.create({
      customer: sub?.stripe_customer_id!,
      return_url: `${APP_BASE_URL}/profile`,
    });
    res.json({ url: session.url });
  } catch (e:any) {
    res.status(500).json({ error: "Could not open billing portal" });
  }
});

4) What goes in Settings (gear) vs Profile (bottom-left)

Profile (what we just built):

Avatar, name, company/brand

Subscription view + billing portal

Preferences (email updates)

Security (password)

Settings (gear, later):

App theme, time zone

Integrations (Google Drive, Stripe customer portal link shortcut)

Notifications center (ties to the bell)

Data export / import (Phase 2+)