Add sponsors system with time slider, LNbits invoices, and UX improvements

- Sponsors table, LNbits createInvoice, webhook handler
- Sponsor routes: create, homepage, list, my-ads, click, extend, check-payment
- Admin routes for sponsor management
- Frontend: SponsorForm, SponsorTimeSlider, SponsorCard, SponsorsSection
- Sponsors page, My Ads page, homepage sponsor block
- Header login dropdown with My Ads, Create Sponsor
- Transactions integration for sponsor payments
- View/click tracking
- OG meta fetch for sponsor images
- Sponsor modal spacing, invoice polling fallback
- Remove Lightning address and Category fields from sponsor form

Made-with: Cursor
This commit is contained in:
Michilis
2026-03-16 00:01:19 +00:00
parent ac9b8dc330
commit dc7007f708
30 changed files with 3123 additions and 68 deletions
+55 -30
View File
@@ -7,6 +7,9 @@ import { ClaimWizard } from "./components/ClaimWizard";
import { StatsSection } from "./components/StatsSection";
import { DepositSection } from "./components/DepositSection";
import { TransactionsPage } from "./pages/TransactionsPage";
import { SponsorsPage } from "./pages/SponsorsPage";
import { MyAdsPage } from "./pages/MyAdsPage";
import { SponsorsSection } from "./components/SponsorsSection";
const FaucetSvg = () => (
<svg className="faucet-svg" viewBox="0 0 100 140" xmlns="http://www.w3.org/2000/svg">
@@ -29,9 +32,13 @@ export default function App() {
const [loginMethod, setLoginMethod] = useState<LoginMethod | null>(null);
const [statsRefetchTrigger, setStatsRefetchTrigger] = useState(0);
useEffect(() => {
const refreshAuth = useCallback(() => {
const token = getToken();
if (!token) return;
if (!token) {
setPubkey(null);
setLoginMethod(null);
return;
}
getAuthMe()
.then((r) => {
setPubkey(r.pubkey);
@@ -44,6 +51,13 @@ export default function App() {
});
}, []);
useEffect(() => {
refreshAuth();
const onAuthChanged = () => refreshAuth();
window.addEventListener("auth-changed", onAuthChanged);
return () => window.removeEventListener("auth-changed", onAuthChanged);
}, [refreshAuth]);
const handlePubkeyChange = useCallback((pk: string | null, method?: LoginMethod) => {
setPubkey(pk);
setLoginMethod(pk ? (method ?? "nip98") : null);
@@ -62,40 +76,33 @@ export default function App() {
return (
<BrowserRouter>
<div className="app">
<Header pubkey={pubkey} onLogout={handleLogout} />
<Header pubkey={pubkey} onLogout={handleLogout} onLoginSuccess={handlePubkeyChange} />
<div className="topbar" />
<div className="app-body">
<Routes>
<Route
path="/"
element={
<div className="container">
<aside className="sidebar sidebar-left">
<div className="funding-panel">
<DepositSection />
</div>
<div className="sidebar-links">
<p className="sidebar-links-title">Sats available</p>
<p className="label">Other Sites:</p>
<a href="https://bitcoin.org/en/" target="_blank" rel="noopener noreferrer">
Bitcoin.org
</a>
<a href="https://bitcoin.org/en/buy" target="_blank" rel="noopener noreferrer">
Bitcoin Market
</a>
</div>
</aside>
<main className="main">
<div className="header">
<FaucetSvg />
<h1>Sats Faucet</h1>
</div>
<ClaimWizard pubkey={pubkey} loginMethod={loginMethod} onPubkeyChange={handlePubkeyChange} onClaimSuccess={handleClaimSuccess} />
</main>
<aside className="sidebar sidebar-right">
<StatsSection refetchTrigger={statsRefetchTrigger} />
</aside>
</div>
<>
<div className="container">
<aside className="sidebar sidebar-left">
<div className="funding-panel">
<DepositSection />
</div>
</aside>
<main className="main">
<div className="header">
<FaucetSvg />
<h1>Sats Faucet</h1>
</div>
<ClaimWizard pubkey={pubkey} loginMethod={loginMethod} onPubkeyChange={handlePubkeyChange} onClaimSuccess={handleClaimSuccess} />
</main>
<aside className="sidebar sidebar-right">
<StatsSection refetchTrigger={statsRefetchTrigger} />
</aside>
</div>
<SponsorsSection />
</>
}
/>
<Route
@@ -108,6 +115,24 @@ export default function App() {
</div>
}
/>
<Route
path="/sponsors"
element={
<div className="sponsors-route">
<SponsorsPage />
</div>
}
/>
<Route
path="/my-ads"
element={
<div className="container container--single">
<main className="main main--full">
<MyAdsPage />
</main>
</div>
}
/>
</Routes>
</div>
<Footer />