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:
@@ -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
@@ -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" />
|
||||
|
||||
+15
-31
@@ -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">
|
||||
<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="main">
|
||||
<div className="header">
|
||||
<FaucetSvg />
|
||||
<h1>Sats Faucet</h1>
|
||||
</div>
|
||||
<ClaimWizard pubkey={pubkey} loginMethod={loginMethod} onPubkeyChange={handlePubkeyChange} onClaimSuccess={handleClaimSuccess} />
|
||||
<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">
|
||||
<div className="sf-page">
|
||||
<TransactionsPage />
|
||||
</main>
|
||||
</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">
|
||||
<div className="sf-page">
|
||||
<MyAdsPage />
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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'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>
|
||||
</p>
|
||||
<div className="claim-wizard-step-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary claim-wizard-btn-primary"
|
||||
onClick={handleClaimClick}
|
||||
>
|
||||
Claim
|
||||
<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>
|
||||
</div>
|
||||
<p className="sf-claim-note">
|
||||
Transaction will be sent instantly via the Lightning Network.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<EligibilityStep
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -17,11 +17,17 @@ 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.
|
||||
<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}
|
||||
@@ -30,6 +36,11 @@ export function ConnectStep({ pubkey, displayName, onConnect, onDisconnect }: Co
|
||||
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">
|
||||
<h4 className="claim-wizard-rules-title">Faucet rules</h4>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+1395
-19
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user