From f797cf944c7be2e29d8819102da9af07d1a0f633 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 14 May 2024 11:17:21 -0700 Subject: [PATCH] Finalize move to module script. --- server/static/js/fireworks.js | 11 +- server/static/js/voterbowl.mjs | 138 +++++++++++++++---- server/vb/components/check_page.py | 42 +++--- server/vb/components/countdown.py | 4 +- server/vb/components/fail_check_partial.js | 22 --- server/vb/components/finish_check_partial.js | 13 -- server/vb/components/ongoing_contest.py | 4 +- server/vb/components/validate_email_page.py | 4 +- 8 files changed, 142 insertions(+), 96 deletions(-) delete mode 100644 server/vb/components/fail_check_partial.js delete mode 100644 server/vb/components/finish_check_partial.js diff --git a/server/static/js/fireworks.js b/server/static/js/fireworks.js index 1a54f06..164b60f 100644 --- a/server/static/js/fireworks.js +++ b/server/static/js/fireworks.js @@ -1,8 +1,7 @@ /** - * name: fireworks-js - * version: 2.10.7 - * author: Vitalij Ryndin (https://crashmax.ru) - * homepage: https://fireworks.js.org - * license MIT + * Bundled by jsDelivr using Rollup v2.79.1 and Terser v5.19.2. + * Original file: /npm/fireworks-js@2.10.7/dist/index.es.js + * + * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files */ -(function(c,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(c=typeof globalThis<"u"?globalThis:c||self,u(c.Fireworks={}))})(this,function(c){"use strict";function u(e){return Math.abs(Math.floor(e))}function p(e,t){return Math.random()*(t-e)+e}function o(e,t){return Math.floor(p(e,t+1))}function g(e,t,i,s){const n=Math.pow;return Math.sqrt(n(e-i,2)+n(t-s,2))}function f(e,t,i=1){if(e>360||e<0)throw new Error(`Expected hue 0-360 range, got \`${e}\``);if(t>100||t<0)throw new Error(`Expected lightness 0-100 range, got \`${t}\``);if(i>1||i<0)throw new Error(`Expected alpha 0-1 range, got \`${i}\``);return`hsla(${e}, 100%, ${t}%, ${i})`}const v=e=>{if(typeof e=="object"&&e!==null){if(typeof Object.getPrototypeOf=="function"){const t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}return Object.prototype.toString.call(e)==="[object Object]"}return!1},b=["__proto__","constructor","prototype"],w=(...e)=>e.reduce((t,i)=>(Object.keys(i).forEach(s=>{b.includes(s)||(Array.isArray(t[s])&&Array.isArray(i[s])?t[s]=i[s]:v(t[s])&&v(i[s])?t[s]=w(t[s],i[s]):t[s]=i[s])}),t),{});function S(e,t){let i;return(...s)=>{i&&clearTimeout(i),i=setTimeout(()=>e(...s),t)}}class O{x;y;ctx;hue;friction;gravity;flickering;lineWidth;explosionLength;angle;speed;brightness;coordinates=[];decay;alpha=1;constructor({x:t,y:i,ctx:s,hue:n,decay:h,gravity:a,friction:r,brightness:l,flickering:d,lineWidth:x,explosionLength:m}){for(this.x=t,this.y=i,this.ctx=s,this.hue=n,this.gravity=a,this.friction=r,this.flickering=d,this.lineWidth=x,this.explosionLength=m,this.angle=p(0,Math.PI*2),this.speed=o(1,10),this.brightness=o(l.min,l.max),this.decay=p(h.min,h.max);this.explosionLength--;)this.coordinates.push([t,i])}update(t){this.coordinates.pop(),this.coordinates.unshift([this.x,this.y]),this.speed*=this.friction,this.x+=Math.cos(this.angle)*this.speed,this.y+=Math.sin(this.angle)*this.speed+this.gravity,this.alpha-=this.decay,this.alpha<=this.decay&&t()}draw(){const t=this.coordinates.length-1;this.ctx.beginPath(),this.ctx.lineWidth=this.lineWidth,this.ctx.fillStyle=f(this.hue,this.brightness,this.alpha),this.ctx.moveTo(this.coordinates[t][0],this.coordinates[t][1]),this.ctx.lineTo(this.x,this.y),this.ctx.strokeStyle=f(this.hue,this.flickering?p(0,this.brightness):this.brightness,this.alpha),this.ctx.stroke()}}class E{constructor(t,i){this.options=t,this.canvas=i,this.pointerDown=this.pointerDown.bind(this),this.pointerUp=this.pointerUp.bind(this),this.pointerMove=this.pointerMove.bind(this)}active=!1;x;y;get mouseOptions(){return this.options.mouse}mount(){this.canvas.addEventListener("pointerdown",this.pointerDown),this.canvas.addEventListener("pointerup",this.pointerUp),this.canvas.addEventListener("pointermove",this.pointerMove)}unmount(){this.canvas.removeEventListener("pointerdown",this.pointerDown),this.canvas.removeEventListener("pointerup",this.pointerUp),this.canvas.removeEventListener("pointermove",this.pointerMove)}usePointer(t,i){const{click:s,move:n}=this.mouseOptions;(s||n)&&(this.x=t.pageX-this.canvas.offsetLeft,this.y=t.pageY-this.canvas.offsetTop,this.active=i)}pointerDown(t){this.usePointer(t,this.mouseOptions.click)}pointerUp(t){this.usePointer(t,!1)}pointerMove(t){this.usePointer(t,this.active)}}class M{hue;rocketsPoint;opacity;acceleration;friction;gravity;particles;explosion;mouse;boundaries;sound;delay;brightness;decay;flickering;intensity;traceLength;traceSpeed;lineWidth;lineStyle;autoresize;constructor(){this.autoresize=!0,this.lineStyle="round",this.flickering=50,this.traceLength=3,this.traceSpeed=10,this.intensity=30,this.explosion=5,this.gravity=1.5,this.opacity=.5,this.particles=50,this.friction=.95,this.acceleration=1.05,this.hue={min:0,max:360},this.rocketsPoint={min:50,max:50},this.lineWidth={explosion:{min:1,max:3},trace:{min:1,max:2}},this.mouse={click:!1,move:!1,max:1},this.delay={min:30,max:60},this.brightness={min:50,max:80},this.decay={min:.015,max:.03},this.sound={enabled:!1,files:["explosion0.mp3","explosion1.mp3","explosion2.mp3"],volume:{min:4,max:8}},this.boundaries={debug:!1,height:0,width:0,x:50,y:50}}update(t){Object.assign(this,w(this,t))}}class z{constructor(t,i){this.options=t,this.render=i}tick=0;rafId=0;fps=60;tolerance=.1;now;mount(){this.now=performance.now();const t=1e3/this.fps,i=s=>{this.rafId=requestAnimationFrame(i);const n=s-this.now;n>=t-this.tolerance&&(this.render(),this.now=s-n%t,this.tick+=n*(this.options.intensity*Math.PI)/1e3)};this.rafId=requestAnimationFrame(i)}unmount(){cancelAnimationFrame(this.rafId)}}class L{constructor(t,i,s){this.options=t,this.updateSize=i,this.container=s}resizer;mount(){if(!this.resizer){const t=S(()=>this.updateSize(),100);this.resizer=new ResizeObserver(t)}this.options.autoresize&&this.resizer.observe(this.container)}unmount(){this.resizer&&this.resizer.unobserve(this.container)}}class T{constructor(t){this.options=t,this.init()}buffers=[];audioContext;onInit=!1;get isEnabled(){return this.options.sound.enabled}get soundOptions(){return this.options.sound}init(){!this.onInit&&this.isEnabled&&(this.onInit=!0,this.audioContext=new(window.AudioContext||window.webkitAudioContext),this.loadSounds())}async loadSounds(){for(const t of this.soundOptions.files){const i=await(await fetch(t)).arrayBuffer();this.audioContext.decodeAudioData(i).then(s=>{this.buffers.push(s)}).catch(s=>{throw s})}}play(){if(this.isEnabled&&this.buffers.length){const t=this.audioContext.createBufferSource(),i=this.buffers[o(0,this.buffers.length-1)],s=this.audioContext.createGain();t.buffer=i,s.gain.value=p(this.soundOptions.volume.min/100,this.soundOptions.volume.max/100),s.connect(this.audioContext.destination),t.connect(s),t.start(0)}else this.init()}}class C{x;y;sx;sy;dx;dy;ctx;hue;speed;acceleration;traceLength;totalDistance;angle;brightness;coordinates=[];currentDistance=0;constructor({x:t,y:i,dx:s,dy:n,ctx:h,hue:a,speed:r,traceLength:l,acceleration:d}){for(this.x=t,this.y=i,this.sx=t,this.sy=i,this.dx=s,this.dy=n,this.ctx=h,this.hue=a,this.speed=r,this.traceLength=l,this.acceleration=d,this.totalDistance=g(t,i,s,n),this.angle=Math.atan2(n-i,s-t),this.brightness=o(50,70);this.traceLength--;)this.coordinates.push([t,i])}update(t){this.coordinates.pop(),this.coordinates.unshift([this.x,this.y]),this.speed*=this.acceleration;const i=Math.cos(this.angle)*this.speed,s=Math.sin(this.angle)*this.speed;this.currentDistance=g(this.sx,this.sy,this.x+i,this.y+s),this.currentDistance>=this.totalDistance?t(this.dx,this.dy,this.hue):(this.x+=i,this.y+=s)}draw(){const t=this.coordinates.length-1;this.ctx.beginPath(),this.ctx.moveTo(this.coordinates[t][0],this.coordinates[t][1]),this.ctx.lineTo(this.x,this.y),this.ctx.strokeStyle=f(this.hue,this.brightness),this.ctx.stroke()}}class y{target;container;canvas;ctx;width;height;traces=[];explosions=[];waitStopRaf;running=!1;opts;sound;resize;mouse;raf;constructor(t,i={}){this.target=t,this.container=t,this.opts=new M,this.createCanvas(this.target),this.updateOptions(i),this.sound=new T(this.opts),this.resize=new L(this.opts,this.updateSize.bind(this),this.container),this.mouse=new E(this.opts,this.canvas),this.raf=new z(this.opts,this.render.bind(this))}get isRunning(){return this.running}get version(){return"2.10.7"}get currentOptions(){return this.opts}start(){this.running||(this.canvas.isConnected||this.createCanvas(this.target),this.running=!0,this.resize.mount(),this.mouse.mount(),this.raf.mount())}stop(t=!1){!this.running||(this.running=!1,this.resize.unmount(),this.mouse.unmount(),this.raf.unmount(),this.clear(),t&&this.canvas.remove())}async waitStop(t){if(!!this.running)return new Promise(i=>{this.waitStopRaf=()=>{!this.waitStopRaf||(requestAnimationFrame(this.waitStopRaf),!this.traces.length&&!this.explosions.length&&(this.waitStopRaf=null,this.stop(t),i()))},this.waitStopRaf()})}pause(){this.running=!this.running,this.running?this.raf.mount():this.raf.unmount()}clear(){!this.ctx||(this.traces=[],this.explosions=[],this.ctx.clearRect(0,0,this.width,this.height))}launch(t=1){for(let i=0;io(t.min,t.max)||this.mouse.active&&i.max>this.traces.length)&&(this.createTrace(),this.raf.tick=0)}drawTrace(){let t=this.traces.length;for(;t--;)this.traces[t].draw(),this.traces[t].update((i,s,n)=>{this.initExplosion(i,s,n),this.sound.play(),this.traces.splice(t,1)})}initExplosion(t,i,s){const{particles:n,flickering:h,lineWidth:a,explosion:r,brightness:l,friction:d,gravity:x,decay:m}=this.opts;let P=u(n);for(;P--;)this.explosions.push(new O({x:t,y:i,ctx:this.ctx,hue:s,friction:d,gravity:x,flickering:o(0,100)<=h,lineWidth:p(a.explosion.min,a.explosion.max),explosionLength:u(r),brightness:l,decay:m}))}drawExplosion(){let t=this.explosions.length;for(;t--;)this.explosions[t].draw(),this.explosions[t].update(()=>{this.explosions.splice(t,1)})}}c.Fireworks=y,c.default=y,Object.defineProperties(c,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); +function t(t){return Math.abs(Math.floor(t))}function i(t,i){return Math.random()*(i-t)+t}function s(t,s){return Math.floor(i(t,s+1))}function e(t,i,s,e){const n=Math.pow;return Math.sqrt(n(t-s,2)+n(i-e,2))}function n(t,i,s=1){if(t>360||t<0)throw new Error(`Expected hue 0-360 range, got \`${t}\``);if(i>100||i<0)throw new Error(`Expected lightness 0-100 range, got \`${i}\``);if(s>1||s<0)throw new Error(`Expected alpha 0-1 range, got \`${s}\``);return`hsla(${t}, 100%, ${i}%, ${s})`}const h=t=>{if("object"==typeof t&&null!==t){if("function"==typeof Object.getPrototypeOf){const i=Object.getPrototypeOf(t);return i===Object.prototype||null===i}return"[object Object]"===Object.prototype.toString.call(t)}return!1},o=["__proto__","constructor","prototype"],a=(...t)=>t.reduce(((t,i)=>(Object.keys(i).forEach((s=>{o.includes(s)||(Array.isArray(t[s])&&Array.isArray(i[s])?t[s]=i[s]:h(t[s])&&h(i[s])?t[s]=a(t[s],i[s]):t[s]=i[s])})),t)),{});class r{x;y;ctx;hue;friction;gravity;flickering;lineWidth;explosionLength;angle;speed;brightness;coordinates=[];decay;alpha=1;constructor({x:t,y:e,ctx:n,hue:h,decay:o,gravity:a,friction:r,brightness:c,flickering:u,lineWidth:p,explosionLength:d}){for(this.x=t,this.y=e,this.ctx=n,this.hue=h,this.gravity=a,this.friction=r,this.flickering=u,this.lineWidth=p,this.explosionLength=d,this.angle=i(0,2*Math.PI),this.speed=s(1,10),this.brightness=s(c.min,c.max),this.decay=i(o.min,o.max);this.explosionLength--;)this.coordinates.push([t,e])}update(t){this.coordinates.pop(),this.coordinates.unshift([this.x,this.y]),this.speed*=this.friction,this.x+=Math.cos(this.angle)*this.speed,this.y+=Math.sin(this.angle)*this.speed+this.gravity,this.alpha-=this.decay,this.alpha<=this.decay&&t()}draw(){const t=this.coordinates.length-1;this.ctx.beginPath(),this.ctx.lineWidth=this.lineWidth,this.ctx.fillStyle=n(this.hue,this.brightness,this.alpha),this.ctx.moveTo(this.coordinates[t][0],this.coordinates[t][1]),this.ctx.lineTo(this.x,this.y),this.ctx.strokeStyle=n(this.hue,this.flickering?i(0,this.brightness):this.brightness,this.alpha),this.ctx.stroke()}}class c{constructor(t,i){this.options=t,this.canvas=i,this.pointerDown=this.pointerDown.bind(this),this.pointerUp=this.pointerUp.bind(this),this.pointerMove=this.pointerMove.bind(this)}active=!1;x;y;get mouseOptions(){return this.options.mouse}mount(){this.canvas.addEventListener("pointerdown",this.pointerDown),this.canvas.addEventListener("pointerup",this.pointerUp),this.canvas.addEventListener("pointermove",this.pointerMove)}unmount(){this.canvas.removeEventListener("pointerdown",this.pointerDown),this.canvas.removeEventListener("pointerup",this.pointerUp),this.canvas.removeEventListener("pointermove",this.pointerMove)}usePointer(t,i){const{click:s,move:e}=this.mouseOptions;(s||e)&&(this.x=t.pageX-this.canvas.offsetLeft,this.y=t.pageY-this.canvas.offsetTop,this.active=i)}pointerDown(t){this.usePointer(t,this.mouseOptions.click)}pointerUp(t){this.usePointer(t,!1)}pointerMove(t){this.usePointer(t,this.active)}}class u{hue;rocketsPoint;opacity;acceleration;friction;gravity;particles;explosion;mouse;boundaries;sound;delay;brightness;decay;flickering;intensity;traceLength;traceSpeed;lineWidth;lineStyle;autoresize;constructor(){this.autoresize=!0,this.lineStyle="round",this.flickering=50,this.traceLength=3,this.traceSpeed=10,this.intensity=30,this.explosion=5,this.gravity=1.5,this.opacity=.5,this.particles=50,this.friction=.95,this.acceleration=1.05,this.hue={min:0,max:360},this.rocketsPoint={min:50,max:50},this.lineWidth={explosion:{min:1,max:3},trace:{min:1,max:2}},this.mouse={click:!1,move:!1,max:1},this.delay={min:30,max:60},this.brightness={min:50,max:80},this.decay={min:.015,max:.03},this.sound={enabled:!1,files:["explosion0.mp3","explosion1.mp3","explosion2.mp3"],volume:{min:4,max:8}},this.boundaries={debug:!1,height:0,width:0,x:50,y:50}}update(t){Object.assign(this,a(this,t))}}class p{constructor(t,i){this.options=t,this.render=i}tick=0;rafId=0;fps=60;tolerance=.1;now;mount(){this.now=performance.now();const t=1e3/this.fps,i=s=>{this.rafId=requestAnimationFrame(i);const e=s-this.now;e>=t-this.tolerance&&(this.render(),this.now=s-e%t,this.tick+=e*(this.options.intensity*Math.PI)/1e3)};this.rafId=requestAnimationFrame(i)}unmount(){cancelAnimationFrame(this.rafId)}}class d{constructor(t,i,s){this.options=t,this.updateSize=i,this.container=s}resizer;mount(){if(!this.resizer){const t=function(t,i){let s;return(...e)=>{s&&clearTimeout(s),s=setTimeout((()=>t(...e)),i)}}((()=>this.updateSize()),100);this.resizer=new ResizeObserver(t)}this.options.autoresize&&this.resizer.observe(this.container)}unmount(){this.resizer&&this.resizer.unobserve(this.container)}}class l{constructor(t){this.options=t,this.init()}buffers=[];audioContext;onInit=!1;get isEnabled(){return this.options.sound.enabled}get soundOptions(){return this.options.sound}init(){!this.onInit&&this.isEnabled&&(this.onInit=!0,this.audioContext=new(window.AudioContext||window.webkitAudioContext),this.loadSounds())}async loadSounds(){for(const t of this.soundOptions.files){const i=await(await fetch(t)).arrayBuffer();this.audioContext.decodeAudioData(i).then((t=>{this.buffers.push(t)})).catch((t=>{throw t}))}}play(){if(this.isEnabled&&this.buffers.length){const t=this.audioContext.createBufferSource(),e=this.buffers[s(0,this.buffers.length-1)],n=this.audioContext.createGain();t.buffer=e,n.gain.value=i(this.soundOptions.volume.min/100,this.soundOptions.volume.max/100),n.connect(this.audioContext.destination),t.connect(n),t.start(0)}else this.init()}}class x{x;y;sx;sy;dx;dy;ctx;hue;speed;acceleration;traceLength;totalDistance;angle;brightness;coordinates=[];currentDistance=0;constructor({x:t,y:i,dx:n,dy:h,ctx:o,hue:a,speed:r,traceLength:c,acceleration:u}){for(this.x=t,this.y=i,this.sx=t,this.sy=i,this.dx=n,this.dy=h,this.ctx=o,this.hue=a,this.speed=r,this.traceLength=c,this.acceleration=u,this.totalDistance=e(t,i,n,h),this.angle=Math.atan2(h-i,n-t),this.brightness=s(50,70);this.traceLength--;)this.coordinates.push([t,i])}update(t){this.coordinates.pop(),this.coordinates.unshift([this.x,this.y]),this.speed*=this.acceleration;const i=Math.cos(this.angle)*this.speed,s=Math.sin(this.angle)*this.speed;this.currentDistance=e(this.sx,this.sy,this.x+i,this.y+s),this.currentDistance>=this.totalDistance?t(this.dx,this.dy,this.hue):(this.x+=i,this.y+=s)}draw(){const t=this.coordinates.length-1;this.ctx.beginPath(),this.ctx.moveTo(this.coordinates[t][0],this.coordinates[t][1]),this.ctx.lineTo(this.x,this.y),this.ctx.strokeStyle=n(this.hue,this.brightness),this.ctx.stroke()}}class g{target;container;canvas;ctx;width;height;traces=[];explosions=[];waitStopRaf;running=!1;opts;sound;resize;mouse;raf;constructor(t,i={}){this.target=t,this.container=t,this.opts=new u,this.createCanvas(this.target),this.updateOptions(i),this.sound=new l(this.opts),this.resize=new d(this.opts,this.updateSize.bind(this),this.container),this.mouse=new c(this.opts,this.canvas),this.raf=new p(this.opts,this.render.bind(this))}get isRunning(){return this.running}get version(){return"2.10.7"}get currentOptions(){return this.opts}start(){this.running||(this.canvas.isConnected||this.createCanvas(this.target),this.running=!0,this.resize.mount(),this.mouse.mount(),this.raf.mount())}stop(t=!1){!this.running||(this.running=!1,this.resize.unmount(),this.mouse.unmount(),this.raf.unmount(),this.clear(),t&&this.canvas.remove())}async waitStop(t){if(this.running)return new Promise((i=>{this.waitStopRaf=()=>{!this.waitStopRaf||(requestAnimationFrame(this.waitStopRaf),!this.traces.length&&!this.explosions.length&&(this.waitStopRaf=null,this.stop(t),i()))},this.waitStopRaf()}))}pause(){this.running=!this.running,this.running?this.raf.mount():this.raf.unmount()}clear(){!this.ctx||(this.traces=[],this.explosions=[],this.ctx.clearRect(0,0,this.width,this.height))}launch(t=1){for(let i=0;is(t.min,t.max)||this.mouse.active&&i.max>this.traces.length)&&(this.createTrace(),this.raf.tick=0)}drawTrace(){let t=this.traces.length;for(;t--;)this.traces[t].draw(),this.traces[t].update(((i,s,e)=>{this.initExplosion(i,s,e),this.sound.play(),this.traces.splice(t,1)}))}initExplosion(e,n,h){const{particles:o,flickering:a,lineWidth:c,explosion:u,brightness:p,friction:d,gravity:l,decay:x}=this.opts;let g=t(o);for(;g--;)this.explosions.push(new r({x:e,y:n,ctx:this.ctx,hue:h,friction:d,gravity:l,flickering:s(0,100)<=a,lineWidth:i(c.explosion.min,c.explosion.max),explosionLength:t(u),brightness:p,decay:x}))}drawExplosion(){let t=this.explosions.length;for(;t--;)this.explosions[t].draw(),this.explosions[t].update((()=>{this.explosions.splice(t,1)}))}}export{g as Fireworks,g as default}; diff --git a/server/static/js/voterbowl.mjs b/server/static/js/voterbowl.mjs index 7ec3656..f94ce3b 100644 --- a/server/static/js/voterbowl.mjs +++ b/server/static/js/voterbowl.mjs @@ -1,4 +1,36 @@ import * as htmx from "./htmx.min.js"; +import { Fireworks } from "./fireworks.js"; + +/*----------------------------------------------------------------- + * API Calls + * -----------------------------------------------------------------*/ + +const api = { + /** + * Finalize a verify and, possibly, mint a new gift ca + * + * @param {string} first_name + * @param {string} last_name + * @param {string} email + * @param {HTMLElement} target + * @returns {Promise} + */ + finishVerify: async (first_name, last_name, email, target) => { + /** @type {HTMLElement|null} */ + try { + await htmx.ajax("POST", "./finish/", { + target, + values: { + first_name, + last_name, + email, + }, + }); + } catch (error) { + console.error(error); + } + }, +}; /*----------------------------------------------------------------- * Check Page Component @@ -40,42 +72,100 @@ class CheckPage extends HTMLElement { console.error("Missing data in event"); return; } - this.finishVerify(data.first_name, data.last_name, data.email); + /** @type {HTMLElement|null} */ + const target = this.querySelector(".urgency"); + if (!target) { + console.error("Missing target element"); + return; + } + api.finishVerify(data.first_name, data.last_name, data.email, target); } }; +} + +customElements.define("check-page", CheckPage); + +/*----------------------------------------------------------------- + * Fail Check Partial + * -----------------------------------------------------------------*/ + +class FailCheck extends HTMLElement { + connectedCallback() { + const { schoolName, firstName, lastName } = this.dataset; + if (!schoolName || !firstName || !lastName) { + console.error("Missing data attributes"); + return; + } + + /** @type {HTMLElement|null} */ + const target = this.querySelector(".urgency"); + if (!target) { + console.error("Missing target element"); + return; + } + + const email = this.demandValidEmail(schoolName, 3); + if (!email) { + console.log("No email provided"); + return; + } + + api.finishVerify(firstName, lastName, email, target); + } /** - * Finalize a verify and, possibly, mint a new gift card if all is well. + * Prompt the user for a valid email address. * - * @param {string} first_name - * @param {string} last_name - * @param {string} email - * @returns {Promise} + * @param {string} schoolName The name of the school. + * @param {number} tries The number of tries to allow. + * @returns {string|null} The email address or null if not provided. + * @private + * @memberof FailCheck */ - async finishVerify(first_name, last_name, email) { - /** @type {HTMLElement|null} */ - const urgency = this.querySelector(".urgency"); - if (!urgency) { - console.error("Missing urgency element"); + demandValidEmail(schoolName, tries) { + /** @type {string|null} */ + let email = null; + let count = 0; + while (email === null && count < tries) { + email = prompt( + `Sorry, but we need your ${schoolName} student email to continue. Please enter it below:` + ); + count++; + } + return email; + } +} + +customElements.define("fail-check", FailCheck); + +/*----------------------------------------------------------------- + * Finish Check Partial + * -----------------------------------------------------------------*/ + +class FinishCheck extends HTMLElement { + connectedCallback() { + // smoothly scroll to the top of the page after a slight delay + setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 100); + + // if the user is a winner, start the fireworks + const { isWinner } = this.dataset; + if (isWinner !== "true") { return; } - try { - await htmx.ajax("POST", "./finish/", { - target: urgency, - values: { - first_name, - last_name, - email, - }, - }); - } catch (error) { - console.error(error); + + // CONSIDER: use of document needed here? + /** @type {HTMLElement|null} */ + const target = document.querySelector(".fireworks"); + if (!target) { + console.error("Missing target element"); + return; } + const fireworks = new Fireworks(target); + fireworks.start(); + setTimeout(() => fireworks.stop(), 10_000); } } -customElements.define("check-page", CheckPage); - /*----------------------------------------------------------------- * Gift code clipboard behavior * -----------------------------------------------------------------*/ diff --git a/server/vb/components/check_page.py b/server/vb/components/check_page.py index d01d6f4..bc31016 100644 --- a/server/vb/components/check_page.py +++ b/server/vb/components/check_page.py @@ -4,7 +4,7 @@ from django.templatetags.static import static from django.urls import reverse -from server.utils.components import js, style +from server.utils.components import style from ..models import Contest, ContestEntry, School from .base_page import base_page @@ -12,8 +12,6 @@ from .logo import school_logo from .utils import Fragment, fragment -check_page_elt = h.Element("check-page", {}, None) - def check_page(school: School, current_contest: Contest | None) -> h.Element: """Render a school-specific 'check voter registration' form page.""" @@ -28,7 +26,7 @@ def check_page(school: School, current_contest: Contest | None) -> h.Element: show_faq=False, show_footer=False, )[ - check_page_elt[ + h.check_page[ h.div[ style( __file__, @@ -70,16 +68,15 @@ def fail_check_partial( """Render a partial page for when the user's email is invalid.""" return fragment[ school_logo(school), - h.p[ - js( - __file__, - "fail_check_partial.js", - school_name=school.short_name, - first_name=first_name, - last_name=last_name, - ), - h.b["We could not use your email"], - f". Please use your { school.short_name } student email.", + h.fail_check( + data_school_name=school.short_name, + data_first_name=first_name, + data_last_name=last_name, + )[ + h.p[ + h.b["We could not use your email"], + f". Please use your { school.short_name } student email.", + ] ], ] @@ -136,13 +133,14 @@ def finish_check_partial( """Render a partial page for when the user has finished the check.""" return fragment[ school_logo(school), - h.p[ - style(__file__, "finish_check_partial.css"), - js( - __file__, - "finish_check_partial.js", - is_winner=contest_entry and contest_entry.is_winner, - ), - _finish_check_description(school, contest_entry, most_recent_winner), + h.finish_check( + data_is_winner="true" + if contest_entry and contest_entry.is_winner + else "false" + )[ + h.p[ + style(__file__, "finish_check_partial.css"), + _finish_check_description(school, contest_entry, most_recent_winner), + ] ], ] diff --git a/server/vb/components/countdown.py b/server/vb/components/countdown.py index 3f9a03b..5cabb4e 100644 --- a/server/vb/components/countdown.py +++ b/server/vb/components/countdown.py @@ -4,8 +4,6 @@ from ..models import Contest -big_countdown = h.Element("big-countdown", {}, None) - def countdown(contest: Contest) -> h.Element: """Render a countdown timer for the given contest.""" @@ -24,7 +22,7 @@ def countdown(contest: Contest) -> h.Element: "giveaway " if contest.is_giveaway else "contest ", "ends in:", ], - big_countdown(data_end_at=contest.end_at.isoformat())[ + h.big_countdown(data_end_at=contest.end_at.isoformat())[ h.div(".countdown")[ h.span(".number", data_number="h0"), h.span(".number", data_number="h1"), diff --git a/server/vb/components/fail_check_partial.js b/server/vb/components/fail_check_partial.js deleted file mode 100644 index 0a5ae54..0000000 --- a/server/vb/components/fail_check_partial.js +++ /dev/null @@ -1,22 +0,0 @@ -function failCheckPartial(self, props) { - const { schoolName, firstName, lastName } = props; - - let email = null; - let count = 0; // give up after 3 tries - while (email === null && count < 3) { - email = prompt(`Sorry, but we need your ${schoolName} student email to continue. Please enter it below:`); - count++; - } - - if (email) { - htmx.ajax("POST", "./finish/", { - target: document.querySelector(".urgency"), - values: { - email: email, - first_name: firstName, - last_name: lastName, - school: schoolName - } - }); - } -} diff --git a/server/vb/components/finish_check_partial.js b/server/vb/components/finish_check_partial.js deleted file mode 100644 index b2850da..0000000 --- a/server/vb/components/finish_check_partial.js +++ /dev/null @@ -1,13 +0,0 @@ -function finishCheckPartial(self, props) { - const { isWinner } = props; - - if (isWinner) { - // @ts-ignore-next-line - const fireworks = new Fireworks.default(document.querySelector(".fireworks")); - fireworks.start(); - setTimeout(() => fireworks.stop(), 10_000); - } - - // smoothly scroll to the top of the page after a slight delay - setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 100); -} diff --git a/server/vb/components/ongoing_contest.py b/server/vb/components/ongoing_contest.py index 5c533e8..5852e23 100644 --- a/server/vb/components/ongoing_contest.py +++ b/server/vb/components/ongoing_contest.py @@ -6,8 +6,6 @@ from .button import button from .logo import school_logo -small_countdown = h.Element("small-countdown", {}, None) - def ongoing_contest(contest: Contest) -> h.Element: """Render an ongoing contest.""" @@ -29,7 +27,7 @@ def ongoing_contest(contest: Contest) -> h.Element: )["Visit event"] ], ], - small_countdown(data_end_at=contest.end_at.isoformat())[ + h.small_countdown(data_end_at=contest.end_at.isoformat())[ h.div(".box countdown")[""] ], ] diff --git a/server/vb/components/validate_email_page.py b/server/vb/components/validate_email_page.py index 841eed0..83455bb 100644 --- a/server/vb/components/validate_email_page.py +++ b/server/vb/components/validate_email_page.py @@ -8,8 +8,6 @@ from .base_page import base_page from .logo import school_logo -gift_code = h.Element("gift-code", {}, None) - def _congrats(contest_entry: ContestEntry, claim_code: str) -> h.Node: return [ @@ -68,7 +66,7 @@ def validate_email_page( main_bg_color=school.logo.bg_color, ), h.main[ - gift_code[ + h.gift_code[ h.div(".container")[ school_logo(school), _congrats(contest_entry, claim_code)