first commit
Made-with: Cursor
This commit is contained in:
75
frontend/src/components/ClaimDenialPanel.tsx
Normal file
75
frontend/src/components/ClaimDenialPanel.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useState } from "react";
|
||||
import { Countdown } from "./Countdown";
|
||||
import type { DenialState } from "../hooks/useClaimFlow";
|
||||
|
||||
const DENIAL_CODE_EXPLANATIONS: Record<string, string> = {
|
||||
cooldown_pubkey: "You've already claimed recently. Each pubkey has a cooldown period.",
|
||||
cooldown_ip: "This IP has reached the claim limit for the cooldown period.",
|
||||
account_too_new: "Your Nostr account is too new. The faucet requires a minimum account age.",
|
||||
low_activity: "Your Nostr profile doesn't meet the minimum activity score (notes, following).",
|
||||
invalid_nip98: "Nostr signature verification failed.",
|
||||
invalid_lightning_address: "The Lightning address format is invalid or could not be resolved.",
|
||||
quote_expired: "The quote expired before confirmation.",
|
||||
payout_failed: "The Lightning payment failed. You can try again.",
|
||||
faucet_disabled: "The faucet is temporarily disabled.",
|
||||
emergency_stop: "The faucet is in emergency stop mode.",
|
||||
insufficient_balance: "The faucet pool has insufficient balance.",
|
||||
daily_budget_exceeded: "The daily payout budget has been reached.",
|
||||
};
|
||||
|
||||
interface ClaimDenialPanelProps {
|
||||
denial: DenialState;
|
||||
onDismiss?: () => void;
|
||||
/** When provided, shows a "Check again" button (e.g. in wizard step 2) */
|
||||
onCheckAgain?: () => void;
|
||||
}
|
||||
|
||||
export function ClaimDenialPanel({ denial, onDismiss, onCheckAgain }: ClaimDenialPanelProps) {
|
||||
const [whyExpanded, setWhyExpanded] = useState(false);
|
||||
const explanation = denial.code ? DENIAL_CODE_EXPLANATIONS[denial.code] ?? null : null;
|
||||
|
||||
return (
|
||||
<div className="claim-denial-panel">
|
||||
<div className="claim-denial-panel-icon" aria-hidden>
|
||||
<span className="claim-denial-panel-icon-inner">⏳</span>
|
||||
</div>
|
||||
<h3 className="claim-denial-panel-title">Not eligible yet</h3>
|
||||
<p className="claim-denial-panel-message">{denial.message}</p>
|
||||
{denial.next_eligible_at != null && (
|
||||
<p className="claim-denial-panel-countdown">
|
||||
Next claim in: <Countdown targetUnixSeconds={denial.next_eligible_at} format="duration" />
|
||||
</p>
|
||||
)}
|
||||
{(denial.code || explanation) && (
|
||||
<div className="claim-denial-panel-why">
|
||||
<button
|
||||
type="button"
|
||||
className="claim-denial-panel-why-trigger"
|
||||
onClick={() => setWhyExpanded((e) => !e)}
|
||||
aria-expanded={whyExpanded}
|
||||
>
|
||||
Why?
|
||||
</button>
|
||||
{whyExpanded && (
|
||||
<div className="claim-denial-panel-why-content">
|
||||
{denial.code && <p className="claim-denial-panel-why-code">Code: {denial.code}</p>}
|
||||
{explanation && <p className="claim-denial-panel-why-text">{explanation}</p>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="claim-denial-panel-actions">
|
||||
{onCheckAgain != null && (
|
||||
<button type="button" className="btn-primary claim-denial-panel-check-again" onClick={onCheckAgain}>
|
||||
Check again
|
||||
</button>
|
||||
)}
|
||||
{onDismiss && (
|
||||
<button type="button" className="btn-secondary claim-denial-panel-dismiss" onClick={onDismiss}>
|
||||
Dismiss
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user