Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 128 additions & 165 deletions src/experiments/ezwallet/EZWalletApp.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -112,186 +112,149 @@
}
</script>

<div class="ezwallet-shell">
<div class="stellar-orbit-bg"></div>
<header class="glass-panel hero-panel">
<p class="kicker">POWERED BY SMOL • STELLAR NATIVE</p>
<h1 class="holo-text">EZWallet</h1>
<p class="hero-tag">Create, Share, and Redeem in One Place.</p>
<p class="hero-sub">Make instantly redeemable tickets for XLM, USDC, or KALE. People can scan, review, and redeem from the same simple flow.</p>
</header>
<div class="wip-banner" role="status" aria-live="polite">
<span class="wip-dot" aria-hidden="true"></span>
<span><strong>Work in Progress</strong> · Until this badge is removed, all payments received are treated as tips supporting ongoing Smol development.</span>
</div>
<div class="ez-shell">
<div class="bg-layer" aria-hidden="true"></div>

<div class="readiness-strip" aria-label="EZWallet readiness status">
<span class="readiness-label">Readiness</span>
<span class="light red active" title="Not ready / test mode">Red</span>
<span class="light yellow" title="Limited readiness">Yellow</span>
<span class="light green" title="Production ready">Green</span>
<span class="readiness-state">Not Ready / Test</span>
</div>
<header class="panel hero">
<p class="eyebrow">EZ Wallet</p>
<h1>Create professional claim links in minutes</h1>
<p class="hero-copy">Build a claim, share a QR, and let people review details before they approve in wallet. Fast for you, clear for them.</p>
</header>

<section class="dashboard-grid">
<article class="glass-panel">
<h2 class="section-title">01 · Build Your Claim Ticket</h2>
<p class="muted">Step 1: pick a coin, add optional details, then generate a ticket.</p>
<div class="label">Asset</div>
<div class="pill-row">
<button class="asset-pill" class:active={asset === 'XLM'} onclick={() => (asset = 'XLM')}>XLM</button>
<button class="asset-pill" class:active={asset === 'USDC'} onclick={() => (asset = 'USDC')}>USDC</button>
<button class="asset-pill" class:active={asset === 'KALE'} onclick={() => (asset = 'KALE')}>KALE</button>
<section class="panel flow">
<h2>1) Claim setup</h2>
<div class="field-group">
<label class="field-label">Asset</label>
<div class="chip-row">
<button class="chip" class:active={asset === 'XLM'} onclick={() => (asset = 'XLM')}>XLM</button>
<button class="chip" class:active={asset === 'USDC'} onclick={() => (asset = 'USDC')}>USDC</button>
<button class="chip" class:active={asset === 'KALE'} onclick={() => (asset = 'KALE')}>KALE</button>
</div>
<label>Mode
<select bind:value={mode}>
<option value="receive_request">Direct claim ticket (one person)</option>
<option value="event_drop">Event ticket drop (multiple people)</option>
</select>
</label>
<label>Amount
<input bind:value={amount} placeholder="Optional (example: 10 or 10.5)" />
</div>

<label class="field-label">Claim type
<select bind:value={mode}>
<option value="receive_request">Direct claim (one person)</option>
<option value="event_drop">Event claim batch (multiple people)</option>
</select>
</label>

<div class="grid-two">
<label class="field-label">Amount
<input bind:value={amount} placeholder="Optional, for example 10 or 10.5" />
</label>
<label>Destination
<input bind:value={destination} placeholder="Optional wallet (G... or C...)" />
<label class="field-label">Destination wallet
<input bind:value={destination} placeholder="Optional: G... or C..." />
</label>
{#if mode === 'event_drop'}
<label>Event
<input bind:value={event} placeholder="Event name" />
</div>

{#if mode === 'event_drop'}
<label class="field-label">Event name
<input bind:value={event} placeholder="Optional event title" />
</label>
{/if}

<label class="field-label">Message
<input bind:value={message} placeholder="Optional message for recipients" />
</label>

<div class="actions">
<button class="btn btn-primary" onclick={makeOne}>Generate claim QR</button>
<button class="btn btn-secondary" onclick={login}>Connect wallet</button>
</div>
<p class="status">{status}</p>
</section>

<section class="panel flow">
<h2>2) Share and preview</h2>
<div class="qr-box">
{#if qrData}
<img src={qrData} alt="Generated claim QR" class="qr" />
{:else}
<p class="muted">Generate a claim to preview the QR here.</p>
{/if}
<label>Message
<input bind:value={message} placeholder="Optional note (what this giveaway is for)" />
</div>
{#if link}
<label class="field-label">Claim link
<input value={link} readonly />
</label>
<div class="action-row">
<button class="holo-button" onclick={makeOne}>Generate QR</button>
<button class="soft-button" onclick={login}>Connect passkey wallet</button>
<div class="actions">
<button class="btn btn-secondary" onclick={() => navigator.clipboard.writeText(link)}>Copy link</button>
<a class="btn btn-secondary" href={link} target="_blank" rel="noreferrer">Open claim page</a>
</div>
<p class="status">{status}</p>
</article>
{/if}
<p class="note">Recipients always review details before approving.</p>
</section>

<article class="glass-panel">
<h2 class="section-title">02 · Share & Review</h2>
<p class="safety">Step 2: share your QR or link. Every person reviews details before redeeming. No auto-send.</p>
<div class="qr-frame">
{#if qrData}
<img src={qrData} alt="Generated Stellar QR intent" class="qr-image" />
{:else}
<p class="muted">Generate a QR to preview it here.</p>
{/if}
</div>
{#if link}
<label>Intent link
<input value={link} readonly />
</label>
<div class="action-row">
<button class="soft-button" onclick={() => navigator.clipboard.writeText(link)}>Copy link</button>
<a class="soft-link" href={link} target="_blank" rel="noreferrer">Open drop</a>
</div>
{/if}
</article>
<section class="panel flow">
<h2>3) Batch generator</h2>
<label class="field-label">Plan
<select bind:value={tier}>
<option value="FREE">Free</option>
<option value="EVENT">Event Pack</option>
<option value="POWER">Power Pack</option>
</select>
</label>

<article class="glass-panel">
<h2 class="section-title">03 · Batch Generation</h2>
<label>Plan
<select bind:value={tier}>
<option value="FREE">Free</option>
<option value="EVENT">Event Pack</option>
<option value="POWER">Power Pack</option>
</select>
</label>
<div class="pill-row">
<button class="soft-button" onclick={() => (count = 1)}>1</button>
<button class="soft-button" onclick={() => (count = 10)} disabled={tier === 'FREE'}>10</button>
<button class="soft-button" onclick={() => (count = 100)} disabled={tier === 'FREE'}>100</button>
<button class="soft-button" onclick={() => (count = 1000)} disabled={tier !== 'POWER'}>1000</button>
</div>
<label>Batch count
<input type="number" bind:value={count} min="1" max={TIERS[tier].maxBatch ?? undefined} />
</label>
<div class="action-row">
<button class="holo-button" onclick={bulk}>Generate batch</button>
{#if csv}<button class="soft-button" onclick={downloadCsv}>Export CSV manifest</button>{/if}
</div>
<p class="muted">Rows generated: {batchRows.length}</p>
<p class="muted">Best for giveaways: generate many tickets at once, then print or distribute digitally.</p>
</article>
<div class="chip-row">
<button class="chip" onclick={() => (count = 1)}>1</button>
<button class="chip" onclick={() => (count = 10)} disabled={tier === 'FREE'}>10</button>
<button class="chip" onclick={() => (count = 100)} disabled={tier === 'FREE'}>100</button>
<button class="chip" onclick={() => (count = 1000)} disabled={tier !== 'POWER'}>1000</button>
</div>

<article class="glass-panel pricing-panel">
<h2 class="section-title">04 · Upgrade & Trust</h2>
<div class="plan-grid">
<div class="plan-card"><strong>Free</strong><span>Up to {TIERS.FREE.maxBatch}</span></div>
<div class="plan-card popular"><strong>Event Pack</strong><span>{TIERS.EVENT.maxBatch} • {TIERS.EVENT.priceXlm} XLM</span></div>
<div class="plan-card"><strong>Power Pack</strong><span>{TIERS.POWER.maxBatch} • {TIERS.POWER.priceXlm} XLM</span></div>
<div class="plan-card"><strong>Custom / Enterprise</strong><span>Contact for event ops scale</span></div>
</div>
<p class="review">Step 4: if needed, upgrade your batch limit. Checkout opens in wallet and you can continue right away.</p>
<div class="action-row">
<button class="holo-button" onclick={checkoutSelectedTier} disabled={tier === 'FREE'}>
{#if tier === 'EVENT'}Upgrade · {TIERS.EVENT.priceXlm} XLM{/if}
{#if tier === 'POWER'}Upgrade · {TIERS.POWER.priceXlm} XLM{/if}
{#if tier === 'FREE'}Select Event or Power to upgrade{/if}
</button>
</div>
<label>Platform destination
<input value={PLATFORM_PAYMENT_DESTINATION} readonly />
</label>
<p class="muted">Anyone can open a ticket safely, review details, and redeem from the same EZWallet flow with signer approval.</p>
</article>
<label class="field-label">Batch size
<input type="number" bind:value={count} min="1" max={TIERS[tier].maxBatch ?? undefined} />
</label>

<div class="actions">
<button class="btn btn-primary" onclick={bulk}>Generate batch</button>
{#if csv}<button class="btn btn-secondary" onclick={downloadCsv}>Download CSV</button>{/if}
</div>

<p class="muted">Generated rows: {batchRows.length}</p>
</section>

<footer class="glass-panel trust-strip">
<span class="trust-chip">Passkey Smart Account</span>
<span class="trust-chip">Public Event Ready</span>
<span class="trust-chip">Review Before Signing</span>
<span class="trust-chip">Stellar Native</span>
<span class="trust-chip">No Seed Phrase Required</span>
<span class="trust-chip">Signer Review Required</span>
</footer>
<section class="panel flow">
<h2>4) Upgrade options</h2>
<div class="plan-grid">
<div class="plan"><strong>Free</strong><span>Up to {TIERS.FREE.maxBatch}</span></div>
<div class="plan"><strong>Event Pack</strong><span>{TIERS.EVENT.maxBatch} • {TIERS.EVENT.priceXlm} XLM</span></div>
<div class="plan"><strong>Power Pack</strong><span>{TIERS.POWER.maxBatch} • {TIERS.POWER.priceXlm} XLM</span></div>
</div>
<div class="actions">
<button class="btn btn-primary" onclick={checkoutSelectedTier} disabled={tier === 'FREE'}>
{#if tier === 'EVENT'}Upgrade to Event Pack{/if}
{#if tier === 'POWER'}Upgrade to Power Pack{/if}
{#if tier === 'FREE'}Select Event or Power{/if}
</button>
</div>
<label class="field-label">Payment destination
<input value={PLATFORM_PAYMENT_DESTINATION} readonly />
</label>
</section>
</div>

<style>
.ezwallet-shell { color: #f4f4f5; max-width: 1200px; margin: 0 auto; padding: 1.25rem; position: relative; }
.stellar-orbit-bg { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(circle at 10% 0%, #1d2330aa, transparent 35%), radial-gradient(circle at 90% 20%, #7dd3fc22, transparent 28%), #050505; z-index: -1; }
.glass-panel { background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03)); border: 1px solid rgba(255,255,255,0.14); border-radius: 18px; padding: 1rem; backdrop-filter: blur(6px); box-shadow: inset 0 1px 0 rgba(255,255,255,0.2); }
.hero-panel { margin-bottom: 1rem; padding: 1.5rem; }
.holo-text { font-size: clamp(2.2rem, 5vw, 4rem); margin: 0; letter-spacing: 0.03em; background: linear-gradient(110deg, #f8fafc 0%, #b7c4d6 18%, #ffffff 34%, #9ee7ff 48%, #d8b4fe 64%, #fff7ad 78%, #cbd5e1 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
.kicker { font-size: 0.72rem; letter-spacing: 0.18em; color: #b8bcc7; margin: 0 0 .5rem; }
.hero-tag { font-size: 1.15rem; margin: .15rem 0; }
.hero-sub { color: #b8bcc7; margin: 0; }

.wip-banner { margin: 0 0 1rem; padding: .65rem .8rem; border-radius: 12px; border: 1px solid rgba(245, 208, 74, .45); background: linear-gradient(120deg, rgba(250, 204, 21, .12), rgba(250, 204, 21, .04), rgba(255,255,255,.02)); color: #f5e7bd; display:flex; gap:.55rem; align-items:center; font-size:.82rem; }
.wip-dot { width: .55rem; height: .55rem; border-radius:999px; background: #facc15; box-shadow: 0 0 8px rgba(250,204,21,.7); }
.readiness-strip { display:flex; align-items:center; gap:.45rem; margin: 0 0 1rem; padding: .55rem .7rem; border-radius: 10px; background: rgba(255,255,255,.03); border:1px solid rgba(255,255,255,.12); font-size:.75rem; }
.readiness-label { color:#cbd5e1; text-transform: uppercase; letter-spacing:.08em; margin-right:.25rem; }
.light { padding:.2rem .5rem; border-radius:999px; border:1px solid transparent; opacity:.45; }
.light.red { background: rgba(239,68,68,.2); color:#fecaca; }
.light.yellow { background: rgba(250,204,21,.18); color:#fde68a; }
.light.green { background: rgba(34,197,94,.18); color:#bbf7d0; }
.light.active { opacity:1; border-color: rgba(255,255,255,.35); box-shadow: 0 0 0 1px rgba(255,255,255,.12) inset; }
.readiness-state { margin-left:auto; color:#fecaca; font-weight:600; }

.dashboard-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
.section-title { margin: 0 0 .85rem; color: #dce2ee; font-size: .95rem; letter-spacing: .12em; text-transform: uppercase; }
label, .label { display: block; font-size: .82rem; color: #b8bcc7; margin: .5rem 0; }
input, select { width: 100%; margin-top: .35rem; background: #0a0a0a; color: #f4f4f5; border: 1px solid rgba(255,255,255,.18); border-radius: 10px; padding: .58rem .66rem; }
input:focus, select:focus, button:focus, a:focus { outline: 2px solid #9ee7ff; outline-offset: 2px; }
.pill-row,.action-row { display:flex; gap:.5rem; flex-wrap:wrap; margin:.5rem 0; }
.asset-pill,.soft-button,.holo-button { border-radius: 999px; border:1px solid rgba(255,255,255,.2); padding:.5rem .85rem; color:#f4f4f5; background:#11131a; transition: all .18s ease; }
.asset-pill.active { border-color: rgba(255,255,255,.35); box-shadow: 0 0 0 1px #9ee7ff44; }
.holo-button { background: linear-gradient(110deg, #f5f5f5, #9ca3af, #e5e7eb, #c0c0c0, #f8fafc, #a3a3a3); color:#060709; font-weight:700; }
button:disabled { opacity:.45; cursor:not-allowed; }
.soft-button:hover,.holo-button:hover,.asset-pill:hover { transform: translateY(-1px); }
.soft-link { align-self:center; color:#dbe5f3; text-decoration:underline; }
.qr-frame { background: linear-gradient(160deg,#1e232e,#11131a); border:1px solid rgba(255,255,255,.35); border-radius:14px; padding: 1rem; min-height: 230px; display:grid; place-items:center; }
.qr-image { background:white; padding:12px; border-radius:10px; width:min(320px,100%); height:auto; }
.muted,.status,.safety,.review { color:#b8bcc7; font-size:.84rem; }
.plan-grid { display:grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap:.6rem; }
.plan-card { border:1px solid rgba(255,255,255,.14); border-radius:12px; padding:.65rem; background: rgba(255,255,255,.03); display:flex; flex-direction:column; gap:.25rem; font-size:.84rem; }
.popular { border-color: rgba(255,255,255,.35); }
.trust-strip { margin-top: 1rem; display:grid; grid-template-columns: repeat(auto-fit, minmax(165px,1fr)); gap:.5rem; }
.trust-chip { border:1px solid rgba(255,255,255,.14); border-radius:10px; padding:.45rem .55rem; font-size:.75rem; color:#c9cfdb; background:#090b10; text-align:center; }
@media (max-width: 900px){ .dashboard-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: no-preference){ .holo-button { background-size: 220% 220%; animation: shimmer 6s linear infinite; } }
@keyframes shimmer { 0% { background-position:0% 50%; } 100% { background-position:200% 50%; } }
.ez-shell { max-width: 1100px; margin: 0 auto; padding: 1rem; color: #f5f5f5; position: relative; display: grid; gap: .85rem; }
.bg-layer { position: fixed; inset: 0; z-index: -1; background: radial-gradient(circle at 5% 5%, #52525b66, transparent 40%), radial-gradient(circle at 90% 0%, #a1a1aa33, transparent 35%), #060606; }
.panel { border: 1px solid rgba(255,255,255,.15); background: linear-gradient(170deg, rgba(255,255,255,.12), rgba(255,255,255,.03)); border-radius: 20px; padding: 1rem; box-shadow: 0 14px 32px rgba(0,0,0,.35); }
.hero h1 { margin: 0; font-size: clamp(1.7rem, 4vw, 2.6rem); background: linear-gradient(100deg, #fff, #d4d4d8, #a1a1aa); -webkit-background-clip: text; background-clip: text; color: transparent; }
.eyebrow { margin: 0 0 .3rem; font-size: .75rem; letter-spacing: .12em; text-transform: uppercase; color: #d4d4d8; }
.hero-copy { margin: .5rem 0 0; color: #d4d4d8; }
.flow h2 { margin: 0 0 .75rem; font-size: 1rem; color: #fafafa; }
.field-label { display: block; margin: .45rem 0; color: #d4d4d8; font-size: .86rem; }
input, select { width: 100%; margin-top: .35rem; border-radius: 10px; border: 1px solid rgba(255,255,255,.22); background: #0d0d0d; color: #fafafa; padding: .58rem .65rem; }
.grid-two { display: grid; gap: .6rem; grid-template-columns: repeat(2, minmax(0,1fr)); }
.chip-row, .actions { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem; }
.chip, .btn { border-radius: 999px; border: 1px solid rgba(255,255,255,.22); padding: .52rem .85rem; background: rgba(255,255,255,.04); color: #fafafa; }
.chip.active { border-color: #e4e4e7; box-shadow: inset 0 0 0 1px #e4e4e7; }
.btn-primary { background: linear-gradient(108deg, #fff, #d4d4d8, #a3a3a3); color: #09090b; font-weight: 700; border-color: transparent; }
.qr-box { border: 1px solid rgba(255,255,255,.2); border-radius: 14px; min-height: 220px; display: grid; place-items: center; background: #0b0b0b; }
.qr { width: min(320px,100%); background: white; padding: 12px; border-radius: 10px; }
.status, .muted, .note { color: #d4d4d8; font-size: .85rem; }
.plan-grid { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); }
.plan { border: 1px solid rgba(255,255,255,.16); border-radius: 12px; padding: .62rem; background: rgba(0,0,0,.26); display:flex; flex-direction:column; gap:.15rem; }
button:disabled { opacity: .45; cursor: not-allowed; }
@media (max-width: 780px) { .grid-two { grid-template-columns: 1fr; } }
</style>
Loading