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 Thumbs.db
# IDE / editor # IDE / editor
.cursor/
.idea/ .idea/
.vscode/ .vscode/
*.swp *.swp
@@ -39,6 +40,9 @@ frontend/dist/
# Deploy config (server-specific) # Deploy config (server-specific)
deploy/ deploy/
# Design assets (local reference only)
design/
# Misc # Misc
*.local *.local
.cache/ .cache/
+13 -1
View File
@@ -6,9 +6,21 @@
<title>Sats Faucet — Free Bitcoin for Nostr Users</title> <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="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="robots" content="index, follow" />
<meta name="theme-color" content="#0c1222" /> <meta name="theme-color" content="#0A0A0B" />
<link rel="canonical" href="https://faucet.lnpulse.app/" /> <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 --> <!-- Open Graph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Sats Faucet — Free Bitcoin for Nostr Users" /> <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 { MyAdsPage } from "./pages/MyAdsPage";
import { SponsorsSection } from "./components/SponsorsSection"; 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() { export default function App() {
const [pubkey, setPubkey] = useState<string | null>(null); const [pubkey, setPubkey] = useState<string | null>(null);
const [loginMethod, setLoginMethod] = useState<LoginMethod | null>(null); const [loginMethod, setLoginMethod] = useState<LoginMethod | null>(null);
@@ -75,7 +59,7 @@ export default function App() {
return ( return (
<BrowserRouter> <BrowserRouter>
<div className="app"> <div className="app sats-flow">
<Header pubkey={pubkey} onLogout={handleLogout} onLoginSuccess={handlePubkeyChange} /> <Header pubkey={pubkey} onLogout={handleLogout} onLoginSuccess={handlePubkeyChange} />
<div className="topbar" /> <div className="topbar" />
<div className="app-body"> <div className="app-body">
@@ -84,23 +68,27 @@ export default function App() {
path="/" path="/"
element={ element={
<> <>
<div className="container"> <section className="claim-flow-page claim-flow-page--grid">
<aside className="sidebar sidebar-left"> <div className="sf-ambient-glow" aria-hidden />
<div className="funding-panel"> <div className="claim-flow-grid">
<DepositSection /> <aside className="sidebar sidebar-left">
</div> <div className="funding-panel">
</aside> <DepositSection />
<main className="main"> </div>
<div className="header"> </aside>
<FaucetSvg /> <main className="claim-flow-main">
<h1>Sats Faucet</h1> <ClaimWizard
</div> pubkey={pubkey}
<ClaimWizard pubkey={pubkey} loginMethod={loginMethod} onPubkeyChange={handlePubkeyChange} onClaimSuccess={handleClaimSuccess} /> loginMethod={loginMethod}
</main> onPubkeyChange={handlePubkeyChange}
<aside className="sidebar sidebar-right"> onClaimSuccess={handleClaimSuccess}
<StatsSection refetchTrigger={statsRefetchTrigger} /> />
</aside> </main>
</div> <aside className="sidebar sidebar-right">
<StatsSection refetchTrigger={statsRefetchTrigger} />
</aside>
</div>
</section>
<SponsorsSection /> <SponsorsSection />
</> </>
} }
@@ -108,17 +96,15 @@ export default function App() {
<Route <Route
path="/transactions" path="/transactions"
element={ element={
<div className="container container--single container--transactions"> <div className="sf-page">
<main className="main main--full"> <TransactionsPage />
<TransactionsPage />
</main>
</div> </div>
} }
/> />
<Route <Route
path="/sponsors" path="/sponsors"
element={ element={
<div className="sponsors-route"> <div className="sponsors-route sf-page">
<SponsorsPage /> <SponsorsPage />
</div> </div>
} }
@@ -126,10 +112,8 @@ export default function App() {
<Route <Route
path="/my-ads" path="/my-ads"
element={ element={
<div className="container container--single"> <div className="sf-page">
<main className="main main--full"> <MyAdsPage />
<MyAdsPage />
</main>
</div> </div>
} }
/> />
+30 -24
View File
@@ -8,8 +8,8 @@ import { EligibilityStep } from "./EligibilityStep";
import { ConfirmStep } from "./ConfirmStep"; import { ConfirmStep } from "./ConfirmStep";
import { SuccessStep } from "./SuccessStep"; import { SuccessStep } from "./SuccessStep";
import { ClaimDenialPanel } from "./ClaimDenialPanel"; import { ClaimDenialPanel } from "./ClaimDenialPanel";
import { DrawAnimation } from "./DrawAnimation";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { ELIGIBILITY_PROGRESS_STEPS } from "../hooks/useClaimFlow";
const LIGHTNING_ADDRESS_REGEX = /^[^@]+@[^@]+$/; const LIGHTNING_ADDRESS_REGEX = /^[^@]+@[^@]+$/;
@@ -148,6 +148,14 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
const quoteExpired = const quoteExpired =
claim.quote != null && claim.quote.expires_at <= Math.floor(Date.now() / 1000); 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(() => { const modalTitle = useMemo(() => {
if (claim.success) return "Sats sent!"; if (claim.success) return "Sats sent!";
if (claim.confirmError) return "Something went wrong"; if (claim.confirmError) return "Something went wrong";
@@ -175,7 +183,7 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
</button> </button>
)} )}
</div> </div>
<StepIndicator currentStep={isConnected ? 2 : 1} /> <StepIndicator currentStep={currentStep} />
</header> </header>
<div className="claim-wizard-body"> <div className="claim-wizard-body">
@@ -187,12 +195,7 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
onDisconnect={handleDisconnect} onDisconnect={handleDisconnect}
/> />
) : claim.loading === "quote" ? ( ) : claim.loading === "quote" ? (
<div className="claim-wizard-body-loading" role="status" aria-live="polite"> <DrawAnimation progressStep={claim.eligibilityProgressStep} />
<div className="claim-wizard-progress-bar" />
<p className="claim-wizard-progress-text">
{ELIGIBILITY_PROGRESS_STEPS[claim.eligibilityProgressStep ?? 0]}
</p>
</div>
) : claim.denial ? ( ) : claim.denial ? (
<ClaimDenialPanel <ClaimDenialPanel
denial={claim.denial} denial={claim.denial}
@@ -200,24 +203,27 @@ export function ClaimWizard({ pubkey, loginMethod, onPubkeyChange, onClaimSucces
onCheckAgain={handleCheckAgain} onCheckAgain={handleCheckAgain}
/> />
) : claim.quote && !quoteExpired ? ( ) : claim.quote && !quoteExpired ? (
<div className="claim-wizard-step claim-wizard-step-eligible"> <div className="claim-wizard-step claim-wizard-step-eligible sf-won">
<h3 className="claim-wizard-step-title">You&apos;re eligible!</h3> <h3 className="sf-won-title">You Won</h3>
<div className="claim-wizard-quote-amount claim-wizard-quote-amount--main"> <div className="sf-won-amount">
<span className="claim-wizard-quote-amount-value">{claim.quote.payout_sats}</span> <span className="sf-won-amount-value">{claim.quote.payout_sats}</span>
<span className="claim-wizard-quote-amount-unit"> sats</span> <span className="sf-won-amount-unit">Sats</span>
</div> </div>
<p className="claim-wizard-quote-destination"> <div className="sf-won-divider" />
To <strong>{lightningAddress}</strong> <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> </p>
<div className="claim-wizard-step-actions">
<button
type="button"
className="btn-primary claim-wizard-btn-primary"
onClick={handleClaimClick}
>
Claim
</button>
</div>
</div> </div>
) : ( ) : (
<EligibilityStep <EligibilityStep
+2 -1
View File
@@ -95,10 +95,11 @@ export function ConfirmStep({
<div className="claim-wizard-step-actions claim-wizard-step-actions--row"> <div className="claim-wizard-step-actions claim-wizard-step-actions--row">
<button <button
type="button" type="button"
className="btn-primary claim-wizard-btn-primary" className="btn-primary claim-wizard-btn-primary claim-wizard-btn-claim"
onClick={onConfirm} onClick={onConfirm}
disabled={quoteExpired} disabled={quoteExpired}
> >
<span className="material-symbols-outlined">bolt</span>
Claim Claim
</button> </button>
<button type="button" className="btn-secondary" onClick={onCancel}> <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 ( return (
<div className="claim-wizard-step claim-wizard-step-connect"> <div className="claim-wizard-step claim-wizard-step-connect sf-connect">
<h3 className="claim-wizard-step-title">Connect your Nostr account</h3> <div className="sf-connect-hero">
<p className="claim-wizard-step-desc"> <div className="sf-connect-badge" aria-hidden>
Sign in with your Nostr key to prove your identity. Your Lightning address can be filled from your profile. <span className="material-symbols-outlined">bolt</span>
</p> </div>
<div className="claim-wizard-connect-cta"> <h3 className="sf-connect-title">Access the Faucet</h3>
<ConnectNostr <p className="sf-connect-subtitle">
pubkey={pubkey} Authenticate securely via Nostr to claim your daily Lightning Network satoshis.
displayName={displayName} </p>
onConnect={onConnect} </div>
onDisconnect={onDisconnect} <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> </div>
{config && ( {config && (
<div className="claim-wizard-rules"> <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 ( return (
<div className="claim-wizard-step claim-wizard-step-eligibility"> <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> <h3 className="claim-wizard-step-title">Check eligibility</h3>
<p className="claim-wizard-step-desc"> <p className="claim-wizard-step-desc">
{readOnly {readOnly
@@ -92,6 +95,7 @@ export function EligibilityStep({
disabled={!canCheck} disabled={!canCheck}
> >
{loading ? "Checking…" : "Check eligibility"} {loading ? "Checking…" : "Check eligibility"}
{!loading && <span className="material-symbols-outlined">arrow_forward</span>}
</button> </button>
</div> </div>
)} )}
+3 -3
View File
@@ -1,8 +1,8 @@
const STEPS = [ const STEPS = [
{ step: 1, label: "Connect" }, { step: 1, label: "Connect" },
{ step: 2, label: "Check" }, { step: 2, label: "Verify" },
{ step: 3, label: "Confirm" }, { step: 3, label: "Draw" },
{ step: 4, label: "Receive" }, { step: 4, label: "Claim" },
] as const; ] as const;
interface StepIndicatorProps { interface StepIndicatorProps {
File diff suppressed because it is too large Load Diff