Redesign frontend with Sats Flow theme and restore homepage sidebars.

Apply the glassmorphic Bitcoin-orange design system across the claim flow and all pages, add the eligibility draw animation, and flank the claim wizard with deposit and stats panels again.
This commit is contained in:
SatsFaucet
2026-06-09 05:23:29 +02:00
parent 6ece47e3ed
commit e7daa57ab4
10 changed files with 1567 additions and 103 deletions
+4
View File
@@ -22,6 +22,7 @@ npm-debug.log*
Thumbs.db
# IDE / editor
.cursor/
.idea/
.vscode/
*.swp
@@ -39,6 +40,9 @@ frontend/dist/
# Deploy config (server-specific)
deploy/
# Design assets (local reference only)
design/
# Misc
*.local
.cache/
+13 -1
View File
@@ -6,9 +6,21 @@
<title>Sats Faucet — Free Bitcoin for Nostr Users</title>
<meta name="description" content="Free Bitcoin sats faucet for Nostr users. Connect your Nostr account, prove your identity, and claim sats via Lightning. Transparent, fair, community-funded." />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#0c1222" />
<meta name="theme-color" content="#0A0A0B" />
<link rel="canonical" href="https://faucet.lnpulse.app/" />
<!-- Fonts: Sats Flow design system -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=swap"
rel="stylesheet"
/>
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Sats Faucet — Free Bitcoin for Nostr Users" />
+27 -43
View File
@@ -11,22 +11,6 @@ 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">
<ellipse cx="52" cy="14" rx="10" ry="6" fill="#c0392b" />
<rect x="49" y="14" width="6" height="16" fill="#c0392b" />
<rect x="30" y="28" width="44" height="30" rx="6" fill="#c0392b" />
<rect x="72" y="36" width="24" height="14" rx="4" fill="#c0392b" />
<rect x="85" y="50" width="12" height="32" rx="4" fill="#c0392b" />
<ellipse cx="89" cy="91" rx="3" ry="5" fill="#5dade2" opacity="0.85" />
<ellipse cx="92" cy="102" rx="2" ry="4" fill="#5dade2" opacity="0.65" />
<ellipse cx="87" cy="108" rx="2" ry="3.5" fill="#5dade2" opacity="0.5" />
<ellipse cx="91" cy="116" rx="1.5" ry="3" fill="#5dade2" opacity="0.35" />
<rect x="10" y="33" width="22" height="18" rx="3" fill="#c0392b" />
<rect x="6" y="36" width="8" height="12" rx="2" fill="#922b21" />
</svg>
);
export default function App() {
const [pubkey, setPubkey] = useState<string | null>(null);
const [loginMethod, setLoginMethod] = useState<LoginMethod | null>(null);
@@ -75,7 +59,7 @@ export default function App() {
return (
<BrowserRouter>
<div className="app">
<div className="app sats-flow">
<Header pubkey={pubkey} onLogout={handleLogout} onLoginSuccess={handlePubkeyChange} />
<div className="topbar" />
<div className="app-body">
@@ -84,23 +68,27 @@ export default function App() {
path="/"
element={
<>
<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>
<section className="claim-flow-page claim-flow-page--grid">
<div className="sf-ambient-glow" aria-hidden />
<div className="claim-flow-grid">
<aside className="sidebar sidebar-left">
<div className="funding-panel">
<DepositSection />
</div>
</aside>
<main className="claim-flow-main">
<ClaimWizard
pubkey={pubkey}
loginMethod={loginMethod}
onPubkeyChange={handlePubkeyChange}
onClaimSuccess={handleClaimSuccess}
/>
</main>
<aside className="sidebar sidebar-right">
<StatsSection refetchTrigger={statsRefetchTrigger} />
</aside>
</div>
</section>
<SponsorsSection />
</>
}
@@ -108,17 +96,15 @@ export default function App() {
<Route
path="/transactions"
element={
<div className="container container--single container--transactions">
<main className="main main--full">
<TransactionsPage />
</main>
<div className="sf-page">
<TransactionsPage />
</div>
}
/>
<Route
path="/sponsors"
element={
<div className="sponsors-route">
<div className="sponsors-route sf-page">
<SponsorsPage />
</div>
}
@@ -126,10 +112,8 @@ export default function App() {
<Route
path="/my-ads"
element={
<div className="container container--single">
<main className="main main--full">
<MyAdsPage />
</main>
<div className="sf-page">
<MyAdsPage />
</div>
}
/>
+30 -24
View File
@@ -8,8 +8,8 @@ import { EligibilityStep } from "./EligibilityStep";
import { ConfirmStep } from "./ConfirmStep";
import { SuccessStep } from "./SuccessStep";
import { ClaimDenialPanel } from "./ClaimDenialPanel";
import { DrawAnimation } from "./DrawAnimation";
import { Modal } from "./Modal";
import { ELIGIBILITY_PROGRESS_STEPS } from "../hooks/useClaimFlow";
const LIGHTNING_ADDRESS_REGEX = /^[^@]+@[^@]+$/;
@@ -148,6 +148,14 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
const quoteExpired =
claim.quote != null && claim.quote.expires_at <= Math.floor(Date.now() / 1000);
const currentStep: 1 | 2 | 3 | 4 = !isConnected
? 1
: claim.loading === "quote"
? 3
: (claim.quote && !quoteExpired) || claim.loading === "confirm" || claim.success
? 4
: 2;
const modalTitle = useMemo(() => {
if (claim.success) return "Sats sent!";
if (claim.confirmError) return "Something went wrong";
@@ -175,7 +183,7 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
</button>
)}
</div>
<StepIndicator currentStep={isConnected ? 2 : 1} />
<StepIndicator currentStep={currentStep} />
</header>
<div className="claim-wizard-body">
@@ -187,12 +195,7 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
onDisconnect={handleDisconnect}
/>
) : claim.loading === "quote" ? (
<div className="claim-wizard-body-loading" role="status" aria-live="polite">
<div className="claim-wizard-progress-bar" />
<p className="claim-wizard-progress-text">
{ELIGIBILITY_PROGRESS_STEPS[claim.eligibilityProgressStep ?? 0]}
</p>
</div>
<DrawAnimation progressStep={claim.eligibilityProgressStep} />
) : claim.denial ? (
<ClaimDenialPanel
denial={claim.denial}
@@ -200,24 +203,27 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
onCheckAgain={handleCheckAgain}
/>
) : claim.quote && !quoteExpired ? (
<div className="claim-wizard-step claim-wizard-step-eligible">
<h3 className="claim-wizard-step-title">You&apos;re eligible!</h3>
<div className="claim-wizard-quote-amount claim-wizard-quote-amount--main">
<span className="claim-wizard-quote-amount-value">{claim.quote.payout_sats}</span>
<span className="claim-wizard-quote-amount-unit"> sats</span>
<div className="claim-wizard-step claim-wizard-step-eligible sf-won">
<h3 className="sf-won-title">You Won</h3>
<div className="sf-won-amount">
<span className="sf-won-amount-value">{claim.quote.payout_sats}</span>
<span className="sf-won-amount-unit">Sats</span>
</div>
<p className="claim-wizard-quote-destination">
To <strong>{lightningAddress}</strong>
<div className="sf-won-divider" />
<div className="sf-destination">
<span className="sf-destination-label">Destination</span>
<div className="sf-destination-chip">
<span className="material-symbols-outlined">bolt</span>
<span className="sf-destination-addr">{lightningAddress}</span>
</div>
</div>
<button type="button" className="sf-claim-btn" onClick={handleClaimClick}>
<span className="material-symbols-outlined">bolt</span>
Claim Sats
</button>
<p className="sf-claim-note">
Transaction will be sent instantly via the Lightning Network.
</p>
<div className="claim-wizard-step-actions">
<button
type="button"
className="btn-primary claim-wizard-btn-primary"
onClick={handleClaimClick}
>
Claim
</button>
</div>
</div>
) : (
<EligibilityStep
+2 -1
View File
@@ -95,10 +95,11 @@ export function ConfirmStep({
<div className="claim-wizard-step-actions claim-wizard-step-actions--row">
<button
type="button"
className="btn-primary claim-wizard-btn-primary"
className="btn-primary claim-wizard-btn-primary claim-wizard-btn-claim"
onClick={onConfirm}
disabled={quoteExpired}
>
<span className="material-symbols-outlined">bolt</span>
Claim
</button>
<button type="button" className="btn-secondary" onClick={onCancel}>
+23 -12
View File
@@ -17,18 +17,29 @@ export function ConnectStep({ pubkey, displayName, onConnect, onDisconnect }: Co
}, []);
return (
<div className="claim-wizard-step claim-wizard-step-connect">
<h3 className="claim-wizard-step-title">Connect your Nostr account</h3>
<p className="claim-wizard-step-desc">
Sign in with your Nostr key to prove your identity. Your Lightning address can be filled from your profile.
</p>
<div className="claim-wizard-connect-cta">
<ConnectNostr
pubkey={pubkey}
displayName={displayName}
onConnect={onConnect}
onDisconnect={onDisconnect}
/>
<div className="claim-wizard-step claim-wizard-step-connect sf-connect">
<div className="sf-connect-hero">
<div className="sf-connect-badge" aria-hidden>
<span className="material-symbols-outlined">bolt</span>
</div>
<h3 className="sf-connect-title">Access the Faucet</h3>
<p className="sf-connect-subtitle">
Authenticate securely via Nostr to claim your daily Lightning Network satoshis.
</p>
</div>
<div className="sf-connect-card">
<div className="claim-wizard-connect-cta">
<ConnectNostr
pubkey={pubkey}
displayName={displayName}
onConnect={onConnect}
onDisconnect={onDisconnect}
/>
</div>
<div className="sf-connect-secure">
<span className="material-symbols-outlined">lock</span>
<span>End-to-end encrypted</span>
</div>
</div>
{config && (
<div className="claim-wizard-rules">
+66
View File
@@ -0,0 +1,66 @@
import { ELIGIBILITY_PROGRESS_STEPS } from "../hooks/useClaimFlow";
interface DrawAnimationProps {
/** 0-based index into ELIGIBILITY_PROGRESS_STEPS, or null. */
progressStep: number | null;
}
const STATUS_CODES = [
"VERIFYING_NODE_STATE",
"CHECKING_COOLDOWN",
"CALCULATING_PAYOUT",
"LOCKING_AMOUNT",
] as const;
const COLUMN_SEEDS = ["837194", "519402", "260473"];
function DrawColumn({ digits }: { digits: string }) {
// Duplicated sequence so the -50% loop is seamless.
const sequence = (digits + digits).split("");
return (
<div className="sf-draw-col-window" aria-hidden>
<div className="sf-draw-column">
{sequence.map((d, i) => (
<span key={i}>{d}</span>
))}
</div>
</div>
);
}
export function DrawAnimation({ progressStep }: DrawAnimationProps) {
const step = Math.min(Math.max(progressStep ?? 0, 0), ELIGIBILITY_PROGRESS_STEPS.length - 1);
const statusCode = STATUS_CODES[step];
const progressPct = ((step + 1) / ELIGIBILITY_PROGRESS_STEPS.length) * 100;
return (
<div className="claim-wizard-step sf-draw" role="status" aria-live="polite">
<div className="sf-draw-icon" aria-hidden>
<span className="material-symbols-outlined">casino</span>
</div>
<h3 className="sf-draw-title">Preparing Draw</h3>
<p className="sf-draw-desc">
Checking your network eligibility and generating your cryptographic proof. Please hold on.
</p>
<div className="sf-draw-ticker">
{COLUMN_SEEDS.map((digits, i) => (
<DrawColumn key={i} digits={digits} />
))}
</div>
<div className="sf-draw-status">
<span className="material-symbols-outlined">sync</span>
<span className="sf-draw-status-text">{statusCode}</span>
</div>
<span className="sf-mono-label" style={{ display: "block", marginBottom: 12 }}>
{ELIGIBILITY_PROGRESS_STEPS[step]}
</span>
<div className="sf-draw-progress">
<div className="sf-draw-progress-fill" style={{ width: `${progressPct}%` }} />
</div>
</div>
);
}
@@ -33,6 +33,9 @@ export function EligibilityStep({
return (
<div className="claim-wizard-step claim-wizard-step-eligibility">
<div className="sf-eligibility-icon" aria-hidden>
<span className="material-symbols-outlined">electric_bolt</span>
</div>
<h3 className="claim-wizard-step-title">Check eligibility</h3>
<p className="claim-wizard-step-desc">
{readOnly
@@ -92,6 +95,7 @@ export function EligibilityStep({
disabled={!canCheck}
>
{loading ? "Checking…" : "Check eligibility"}
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button>
</div>
)}
+3 -3
View File
@@ -1,8 +1,8 @@
const STEPS = [
{ step: 1, label: "Connect" },
{ step: 2, label: "Check" },
{ step: 3, label: "Confirm" },
{ step: 4, label: "Receive" },
{ step: 2, label: "Verify" },
{ step: 3, label: "Draw" },
{ step: 4, label: "Claim" },
] as const;
interface StepIndicatorProps {
File diff suppressed because it is too large Load Diff