Back to Let's Napp!
SOURCE / PINNED RELEASE

Made of little things.

Let's Napp!

Release
64e1e1eb905d…
Author-recorded commit
Not provided
License
Check the original source

The hash-verified playable HTML, which may contain bundled or minified code. This view does not execute it.

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="theme-color" content="#090b24" />
    <title>Let's Napp!</title>
    <script type="module" crossorigin>function F(){const t=new Map;function e(n,i){if(t.has(n))throw new Error(`NAP domain "${n}" is already registered`);t.set(n,i)}function a(n){const i=n.type.indexOf(".");if(i<=0)return!1;const c=n.type.slice(0,i),h=t.get(c);return h?(h(n),!0):!1}function o(){return Array.from(t.keys())}return{registerNap:e,dispatch:a,getRegisteredDomains:o}}var K=F(),N=K.registerNap,G="relay";function L(){var e;const t=window;if(!((e=t.napplet)!=null&&e.relay))throw new Error("window.napplet.relay is unavailable -- runtime did not inject the relay domain");return t.napplet.relay}function Y(t,e,a,o){return L().subscribe(t,e,a,o)}function O(t){return L().query(t)}N(G,t=>{});var z="identity";function R(){var e;const t=window;if(!((e=t.napplet)!=null&&e.identity))throw new Error("window.napplet.identity is unavailable -- runtime did not inject this domain");return t.napplet.identity}function H(){return R().getPublicKey()}function Q(t){return R().onChanged(t)}N(z,t=>{});var V="theme";function $(){var e;const t=window;if(!((e=t.napplet)!=null&&e.theme))throw new Error("window.napplet.theme is unavailable -- runtime did not inject this domain");return t.napplet.theme}function J(){return $().get()}function W(t){return $().onChanged(t)}N(V,t=>{});var X="resource";function Z(){var e;const t=window;if(!((e=t.napplet)!=null&&e.resource))throw new Error("window.napplet.resource is unavailable -- runtime did not inject this domain");return t.napplet.resource}function tt(t,e){return Z().bytes(t,e)}N(X,t=>{});var et="outbox";function I(){var e;const t=window;if(!((e=t.napplet)!=null&&e.outbox))throw new Error("window.napplet.outbox is unavailable -- runtime did not inject this domain");return t.napplet.outbox}function q(t,e){return I().query(t,e)}function nt(t,e){return I().subscribe(t,e)}function ot(t,e){return I().publish(t,e)}N(et,t=>{});const y=34235,T=0,m=100,M=6e3,U=["wss://nostr.mom","wss://nos.lol"],r={connectionStatus:l("#connectionStatus"),greeting:l("#greeting"),napButton:l("#napButton"),napButtonLabel:l("#napButtonLabel"),helperText:l("#helperText"),napperCount:l("#napperCount"),napperGrid:l("#napperGrid"),emptyState:l("#emptyState"),routeBadge:l("#routeBadge")},g=new Map,v=new Map,E=new Map,A=new Set;let u=null,p="none",w=!1,f=null,C=null,k=null,S=null;function l(t){const e=document.querySelector(t);if(!e)throw new Error(`Missing required element: ${t}`);return e}function b(t){var e;return!!((e=window.napplet)!=null&&e[t])}function d(t,e,a=M){return new Promise((o,n)=>{const i=window.setTimeout(()=>n(new Error(`${e} timed out`)),a);t.then(c=>{window.clearTimeout(i),o(c)},c=>{window.clearTimeout(i),n(c)})})}function D(t){const e=document.documentElement;e.style.setProperty("--shell-bg",t.colors.background),e.style.setProperty("--shell-text",t.colors.text),e.style.setProperty("--shell-primary",t.colors.primary)}async function at(){if(b("theme"))try{D(await d(J(),"Theme")),S=W(D)}catch{}}function P(t){return!!(t!=null&&t.content.trim())}function x(t){if(t.kind!==y)return;const e=g.get(t.pubkey);e&&e.created_at>t.created_at||e&&e.created_at===t.created_at&&e.id>=t.id||(g.set(t.pubkey,t),s(),lt())}function j(){return[...g.values()].filter(t=>P(t)).sort((t,e)=>e.created_at-t.created_at)}function _(t){return`${t.slice(0,6)}…${t.slice(-4)}`}function rt(t){const e=Math.max(0,Math.floor(Date.now()/1e3-t)/60);return e<1?"just drifted off":e<60?`napping ${Math.floor(e)}m`:`napping ${Math.floor(e/60)}h ${Math.floor(e%60)}m`}function it(t,e){const a=document.createElement("div");a.className="avatar-shell";const o=E.get(t);if(o){const n=document.createElement("img");n.src=o,n.alt="",a.append(n)}else{const n=document.createElement("span");n.textContent=((e==null?void 0:e.name.at(0))??"☾").toUpperCase(),a.append(n)}return a}function st(t,e){const a=v.get(t.pubkey),o=document.createElement("article");o.className="napper-card",t.pubkey===u&&o.classList.add("is-you"),o.style.setProperty("--delay",`${e%6*.22}s`);const n=document.createElement("div");n.className="dream",n.setAttribute("aria-hidden","true"),n.innerHTML="<i>z</i><i>z</i><i>z</i>";const i=it(t.pubkey,a),c=document.createElement("div");c.className="napper-details";const h=document.createElement("h3");h.textContent=t.pubkey===u?`${(a==null?void 0:a.name)??"You"} (you)`:(a==null?void 0:a.name)??_(t.pubkey);const B=document.createElement("p");return B.textContent=rt(t.created_at),c.append(h,B),o.append(n,i,c),o}function s(){const t=j(),e=u?P(g.get(u)):!1;r.napperCount.textContent=String(t.length),r.emptyState.hidden=t.length>0,r.napperGrid.replaceChildren(...t.map(st)),r.napButton.classList.toggle("is-napping",e),r.napButtonLabel.textContent=w?"Sharing…":e?"I'm awake now":"I'm taking a nap",r.greeting.textContent=e?"Sleep mode is on.":"Ready for some rest?",r.helperText.textContent=u?p!=="outbox"?"You can browse here, but this app cannot share your status.":e?"Sweet dreams. Tap again when you wake up.":"Your nap status is shared through your Nostr outbox.":"Sign in to your Nostr app to share a nap.",r.napButton.disabled=w||!u||p!=="outbox"}function ut(t){try{const e=JSON.parse(t.content),a=typeof e.display_name=="string"?e.display_name.trim():"",o=typeof e.name=="string"?e.name.trim():"",n=typeof e.picture=="string"&&e.picture.trim()?e.picture.trim():void 0;return{name:a||o||_(t.pubkey),picture:n}}catch{return null}}async function ct(t,e){if(!(!b("resource")||A.has(t)||E.has(t))){A.add(t);try{const a=await d(tt(e),"Avatar",8e3),o=URL.createObjectURL(a);E.set(t,o),s()}catch{}finally{A.delete(t)}}}async function lt(){const t=j().map(e=>e.pubkey).filter(e=>!v.has(e));if(t.length!==0)try{const e=p==="outbox"?(await d(q({kinds:[T],authors:t},{authors:t,limit:t.length,timeoutMs:M}),"Profiles")).events:p==="relay"?await d(O({kinds:[T],authors:t}),"Profiles"):[],a=new Map;for(const{event:o}of e){const n=a.get(o.pubkey);(!n||o.created_at>n.created_at)&&a.set(o.pubkey,o)}for(const o of t){const n=a.get(o),i=n?ut(n):null;v.set(o,i??{name:_(o)}),i!=null&&i.picture&&ct(o,i.picture)}s()}catch{for(const e of t)v.set(e,{name:_(e)});s()}}async function pt(){if(!b("identity")){r.connectionStatus.textContent="Sign-in unavailable",s();return}const t=e=>{u=e||null,s()};try{t(await d(H(),"Identity")),k=Q(t)}catch{u=null,s()}}function dt(){f=nt({kinds:[y],limit:m},{relays:U,limit:m,timeoutMs:M}),f.on("event",({event:t})=>x(t)),f.on("closed",()=>{r.connectionStatus.textContent="Feed paused"})}function mt(){C=Y({kinds:[y],limit:m},({event:t})=>x(t),()=>{r.connectionStatus.textContent="Fallback feed live"})}async function ft(){if(b("outbox")){p="outbox",r.routeBadge.textContent="LIVE",r.connectionStatus.textContent="Finding dreamers…";try{const t=await d(q({kinds:[y],limit:m},{relays:U,limit:m,timeoutMs:M}),"Nap feed");for(const{event:e}of t.events)x(e);dt(),r.connectionStatus.textContent=t.incomplete?"Partial dream feed":"Dream feed live";return}catch{}}if(b("relay")){p="relay",r.routeBadge.textContent="BACKUP FEED",r.connectionStatus.textContent="Using fallback feed";try{const t=await d(O({kinds:[y],limit:m}),"Fallback feed");for(const{event:e}of t)x(e);mt(),r.connectionStatus.textContent="Fallback feed live",s();return}catch{}}p="none",r.routeBadge.textContent="UNAVAILABLE",r.connectionStatus.textContent="Feed unavailable",r.emptyState.querySelector("p").textContent="This Nostr app cannot load the dream feed.",r.emptyState.querySelector("small").textContent="Try opening the napplet in an updated, compatible app.",s()}async function yt(){if(!u||p!=="outbox"||w)return;w=!0,s();const t=P(g.get(u));try{const e=await d(ot({kind:y,content:t?"":"napping",tags:[],created_at:Math.floor(Date.now()/1e3)}),"Nap status",12e3);if(!e.ok)throw new Error(e.error??"Publish failed");e.event&&x(e.event),r.connectionStatus.textContent=t?"Welcome back":"Sweet dreams"}catch{r.connectionStatus.textContent="Could not share status"}finally{w=!1,s()}}function ht(){f==null||f.close(),C==null||C.close(),k==null||k.close(),S==null||S.close();for(const t of E.values())URL.revokeObjectURL(t)}async function wt(){r.napButton.addEventListener("click",()=>void yt()),window.addEventListener("pagehide",ht,{once:!0}),s(),await Promise.all([at(),pt(),ft()]),s()}wt();</script>
    <style rel="stylesheet" crossorigin>:root{color-scheme:dark;--shell-bg: #090b24;--shell-text: #f7f2ff;--shell-primary: #b9a6ff;--ink-soft: color-mix(in srgb, var(--shell-text) 68%, transparent);--panel: color-mix(in srgb, var(--shell-bg) 74%, white 8%);--line: color-mix(in srgb, var(--shell-text) 14%, transparent);font-family:Inter,ui-rounded,SF Pro Rounded,system-ui,-apple-system,sans-serif;font-synthesis:none;background:var(--shell-bg);color:var(--shell-text)}*{box-sizing:border-box}html,body{min-width:0;min-height:100%;margin:0}body{min-height:100vh;overflow-x:hidden;background:radial-gradient(circle at 78% 14%,color-mix(in srgb,var(--shell-primary) 25%,transparent),transparent 28rem),radial-gradient(circle at 10% 80%,#27306b55,transparent 28rem),var(--shell-bg)}button{font:inherit}.app-shell{position:relative;isolation:isolate;width:min(100%,1120px);min-height:100vh;margin:0 auto;padding:clamp(1rem,4vw,3.25rem);display:flex;flex-direction:column;gap:clamp(2rem,6vw,5rem)}.stars,.stars:before,.stars:after{position:fixed;top:0;right:0;bottom:0;left:0;z-index:-1;pointer-events:none;content:"";opacity:.5;background-image:radial-gradient(circle,#fff 0 1px,transparent 1.4px),radial-gradient(circle,var(--shell-primary) 0 1px,transparent 1.5px);background-position:20px 30px,90px 115px;background-size:154px 171px,210px 233px}.stars:before{transform:translate(43px,69px) scale(.72);opacity:.25}.stars:after{transform:translate(-33px,8px) scale(1.3);opacity:.13}.masthead,.section-heading,footer{display:flex;align-items:center;justify-content:space-between;gap:1rem}.brand{display:flex;align-items:center;gap:.8rem}.brand-moon{color:var(--shell-primary);font-size:2.15rem;filter:drop-shadow(0 0 12px var(--shell-primary))}.eyebrow{margin:0 0 .24rem;color:var(--shell-primary);font-size:.68rem;font-weight:800;letter-spacing:.2em;text-transform:uppercase}h1,h2,h3,p{margin-top:0}h1{margin-bottom:0;font-size:clamp(1rem,2vw,1.25rem);letter-spacing:-.03em}.connection,.route-badge{border:1px solid var(--line);border-radius:99px;padding:.5rem .75rem;color:var(--ink-soft);background:color-mix(in srgb,var(--shell-bg) 58%,transparent);font-size:.7rem;letter-spacing:.06em;white-space:nowrap}.connection:before{content:"";display:inline-block;width:.45rem;height:.45rem;margin-right:.45rem;border-radius:50%;background:#8be5b1;box-shadow:0 0 10px #8be5b1}.hero{display:grid;grid-template-columns:minmax(13rem,.86fr) minmax(17rem,1.14fr);align-items:center;gap:clamp(2rem,8vw,7rem);min-height:min(54vh,560px)}.moon-wrap{position:relative;display:grid;place-items:center;aspect-ratio:1}.moon{display:grid;place-items:center;width:clamp(11rem,27vw,19rem);aspect-ratio:1;border-radius:50%;color:#211d45;background:linear-gradient(145deg,#fffef5,#dddcff 70%,#b5a6e4);box-shadow:0 0 28px #c5b6ff99,0 0 95px #9e88ff55,inset -24px -18px 40px #9f8ddc66;animation:float 6s ease-in-out infinite}.moon span{font-family:Georgia,serif;font-size:clamp(7rem,17vw,13rem);line-height:.6;transform:translate(-7%,-2%) rotate(-12deg)}.orbit{position:absolute;border:1px solid color-mix(in srgb,var(--shell-primary) 25%,transparent);border-radius:50%;transform:rotate(-18deg)}.orbit-one{width:94%;height:37%}.orbit-two{width:75%;height:28%;transform:rotate(42deg);opacity:.4}.greeting{color:var(--shell-primary);font-weight:700}.hero h2{margin-bottom:1.8rem;font-family:Georgia,Times New Roman,serif;font-size:clamp(2.7rem,7vw,5.6rem);font-weight:400;line-height:.91;letter-spacing:-.055em}.hero h2 em{color:var(--shell-primary);font-weight:400}.nap-button{display:inline-flex;align-items:center;justify-content:center;gap:.7rem;min-height:3.65rem;padding:.9rem 1.45rem;border:1px solid color-mix(in srgb,var(--shell-primary) 50%,transparent);border-radius:1.15rem;color:#171329;background:linear-gradient(135deg,#f3eeff,var(--shell-primary));box-shadow:0 12px 36px color-mix(in srgb,var(--shell-primary) 25%,transparent);font-weight:850;cursor:pointer;transition:transform .2s,box-shadow .2s,opacity .2s}.nap-button:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 16px 44px color-mix(in srgb,var(--shell-primary) 38%,transparent)}.nap-button:focus-visible{outline:3px solid white;outline-offset:4px}.nap-button:disabled{cursor:not-allowed;opacity:.5}.nap-button.is-napping{color:var(--shell-text);background:color-mix(in srgb,var(--shell-bg) 72%,var(--shell-primary))}.button-icon{font-size:1.35rem}.helper{margin:.9rem 0 0;max-width:25rem;color:var(--ink-soft);font-size:.82rem;line-height:1.5}.sleepers{padding:clamp(1rem,3vw,2rem);border:1px solid var(--line);border-radius:2rem;background:linear-gradient(145deg,color-mix(in srgb,var(--panel) 95%,transparent),color-mix(in srgb,var(--shell-bg) 75%,transparent));box-shadow:0 28px 80px #0003;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px)}.section-heading{margin-bottom:1.5rem}.section-heading h2{margin:0;font-family:Georgia,serif;font-size:clamp(1.55rem,4vw,2.5rem);font-weight:400}.route-badge{color:var(--shell-primary);font-weight:800}.napper-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:.8rem}.napper-card{position:relative;display:flex;align-items:center;gap:.85rem;min-width:0;padding:1rem;border:1px solid var(--line);border-radius:1.3rem;background:color-mix(in srgb,var(--shell-bg) 72%,white 5%)}.napper-card.is-you{border-color:color-mix(in srgb,var(--shell-primary) 50%,transparent)}.avatar-shell{flex:0 0 auto;display:grid;place-items:center;width:3.25rem;height:3.25rem;overflow:hidden;border:2px solid color-mix(in srgb,var(--shell-primary) 60%,transparent);border-radius:50%;color:var(--shell-bg);background:linear-gradient(145deg,white,var(--shell-primary));font-weight:900}.avatar-shell img{width:100%;height:100%;object-fit:cover}.napper-details{min-width:0}.napper-details h3{overflow:hidden;margin:0 0 .2rem;font-size:.9rem;text-overflow:ellipsis;white-space:nowrap}.napper-details p{margin:0;color:var(--ink-soft);font-size:.72rem}.dream{position:absolute;top:-.7rem;left:2.65rem;color:var(--shell-primary);font-family:Georgia,serif;font-weight:800}.dream i{position:absolute;font-style:italic;animation:dream 3s ease-in-out infinite;animation-delay:calc(var(--delay) + var(--i, 0s))}.dream i:nth-child(2){--i: .65s;left:.85rem;top:-.6rem;font-size:.8rem}.dream i:nth-child(3){--i: 1.3s;left:1.55rem;top:-1.25rem;font-size:.65rem}.empty-state{padding:clamp(2rem,6vw,4rem) 1rem;text-align:center;color:var(--ink-soft)}.empty-state[hidden]{display:none}.empty-state>span{display:block;margin-bottom:.6rem;color:var(--shell-primary);font-size:3.2rem}.empty-state p{margin-bottom:.3rem;color:var(--shell-text);font-family:Georgia,serif;font-size:1.35rem}.empty-state small{font-size:.75rem}footer{justify-content:center;color:var(--ink-soft);font-size:.68rem;letter-spacing:.12em;text-transform:uppercase}@keyframes float{50%{transform:translateY(-10px) rotate(2deg)}}@keyframes dream{0%,to{opacity:0;transform:translateY(5px) scale(.7)}35%{opacity:1}70%{opacity:.35;transform:translate(8px,-13px) scale(1.15)}}@media(max-width:660px){.app-shell{gap:2.25rem}.hero{grid-template-columns:1fr;gap:.6rem;text-align:center}.moon-wrap{max-height:15rem}.moon{width:10.5rem}.moon span{font-size:7rem}.hero-copy{display:flex;flex-direction:column;align-items:center}.hero h2{font-size:clamp(2.55rem,14vw,4.2rem)}}@media(max-width:360px),(max-height:430px){.app-shell{min-height:100vh;padding:.7rem;gap:.65rem}.masthead .eyebrow,.connection,.greeting,.hero h2,.helper,footer,.section-heading .eyebrow{display:none}.masthead{min-height:2rem}.hero{display:flex;min-height:auto;gap:.65rem}.moon-wrap{flex:0 0 3.5rem;width:3.5rem;height:3.5rem}.moon{width:3.2rem}.moon span{font-size:2.2rem}.orbit{display:none}.hero-copy{flex:1}.nap-button{width:100%;min-height:3.2rem;padding:.55rem;border-radius:.9rem}.sleepers{flex:1;min-height:0;padding:.75rem;border-radius:1rem}.section-heading{margin-bottom:.55rem}.section-heading h2{font-size:1rem}.route-badge{padding:.28rem .45rem;font-size:.55rem}.napper-grid{display:flex;gap:.45rem;overflow-x:auto}.napper-card{flex:0 0 10rem;padding:.55rem;border-radius:.8rem}.avatar-shell{width:2.2rem;height:2.2rem}.empty-state{padding:1rem .4rem}.empty-state>span{margin:0;font-size:1.8rem}.empty-state p{margin-bottom:.15rem;font-size:.92rem}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{scroll-behavior:auto!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}</style>
    <meta name="napplet-type" content="letsnapp">
  </head>
  <body>
    <main id="app" class="app-shell">
      <div class="stars" aria-hidden="true"></div>

      <header class="masthead">
        <div class="brand">
          <span class="brand-moon" aria-hidden="true">☾</span>
          <div>
            <p class="eyebrow">quiet hours</p>
            <h1>Let's Napp!</h1>
          </div>
        </div>
        <output id="connectionStatus" class="connection" aria-live="polite">Waking up…</output>
      </header>

      <section class="hero" aria-labelledby="nap-heading">
        <div class="moon-wrap" aria-hidden="true">
          <div class="moon"><span>☾</span></div>
          <i class="orbit orbit-one"></i>
          <i class="orbit orbit-two"></i>
        </div>

        <div class="hero-copy">
          <p id="greeting" class="greeting">Ready for some rest?</p>
          <h2 id="nap-heading">Tell the world<br />you're <em>napping.</em></h2>
          <button id="napButton" class="nap-button" type="button" disabled>
            <span class="button-icon" aria-hidden="true">☾</span>
            <span id="napButtonLabel">Checking identity…</span>
          </button>
          <p id="helperText" class="helper">Your nap status is shared through your Nostr outbox.</p>
        </div>
      </section>

      <section class="sleepers" aria-labelledby="sleepers-title">
        <header class="section-heading">
          <div>
            <p class="eyebrow">dream team</p>
            <h2 id="sleepers-title"><span id="napperCount">0</span> napping now</h2>
          </div>
          <span id="routeBadge" class="route-badge">OUTBOX</span>
        </header>

        <div id="napperGrid" class="napper-grid" aria-live="polite"></div>
        <div id="emptyState" class="empty-state">
          <span aria-hidden="true">☁</span>
          <p>No one is napping yet.</p>
          <small>You could be the first to drift off.</small>
        </div>
      </section>

      <footer>
        <span>Kind 34235</span>
        <span aria-hidden="true">·</span>
        <span>Rest well</span>
      </footer>
    </main>
  </body>
</html>