馃 distributed transcription service thistle.dunkirk.sh
1import { css, html, LitElement } from "lit"; 2import { customElement, state } from "lit/decorators.js"; 3 4declare global { 5 interface Window { 6 confetti: (options: { 7 particleCount?: number; 8 spread?: number; 9 startVelocity?: number; 10 decay?: number; 11 scalar?: number; 12 origin?: { x?: number; y?: number }; 13 }) => void; 14 } 15} 16 17@customElement("checkout-success") 18export class CheckoutSuccess extends LitElement { 19 @state() checkoutId: string | null = null; 20 @state() loading = true; 21 @state() error = ""; 22 23 static override styles = css` 24 :host { 25 display: block; 26 max-width: 48rem; 27 margin: 0 auto; 28 padding: 2rem; 29 } 30 31 .success-container { 32 text-align: center; 33 padding: 3rem 2rem; 34 } 35 36 .success-icon { 37 font-size: 5rem; 38 margin-bottom: 1.5rem; 39 animation: bounce 0.6s ease-out; 40 } 41 42 @keyframes bounce { 43 0%, 100% { transform: translateY(0); } 44 50% { transform: translateY(-20px); } 45 } 46 47 h1 { 48 color: var(--text); 49 margin-bottom: 1rem; 50 font-size: 2.5rem; 51 } 52 53 .message { 54 color: var(--text); 55 opacity: 0.8; 56 margin-bottom: 2rem; 57 line-height: 1.8; 58 font-size: 1.125rem; 59 } 60 61 .highlight { 62 color: var(--accent); 63 font-weight: 600; 64 } 65 66 .features { 67 background: var(--background); 68 border: 1px solid var(--secondary); 69 border-radius: 12px; 70 padding: 2rem; 71 margin: 2rem 0; 72 text-align: left; 73 } 74 75 .features h2 { 76 color: var(--text); 77 font-size: 1.25rem; 78 margin: 0 0 1.5rem 0; 79 text-align: center; 80 } 81 82 .feature-list { 83 list-style: none; 84 padding: 0; 85 margin: 0; 86 display: grid; 87 gap: 1rem; 88 } 89 90 .feature-item { 91 display: flex; 92 align-items: center; 93 gap: 0.75rem; 94 color: var(--text); 95 font-size: 1rem; 96 } 97 98 .feature-icon { 99 font-size: 1.5rem; 100 flex-shrink: 0; 101 } 102 103 .checkout-id { 104 background: var(--background); 105 border: 1px solid var(--secondary); 106 border-radius: 8px; 107 padding: 1rem; 108 margin: 2rem 0; 109 font-family: monospace; 110 font-size: 0.875rem; 111 color: var(--text); 112 opacity: 0.6; 113 word-break: break-all; 114 } 115 116 .actions { 117 display: flex; 118 gap: 1rem; 119 justify-content: center; 120 flex-wrap: wrap; 121 margin-top: 2rem; 122 } 123 124 .btn { 125 padding: 0.75rem 1.5rem; 126 border-radius: 6px; 127 font-size: 1rem; 128 font-weight: 500; 129 cursor: pointer; 130 transition: all 0.2s; 131 font-family: inherit; 132 border: 2px solid transparent; 133 text-decoration: none; 134 display: inline-block; 135 } 136 137 .btn-affirmative { 138 background: var(--primary); 139 color: white; 140 border-color: var(--primary); 141 } 142 143 .btn-affirmative:hover { 144 background: var(--gunmetal); 145 border-color: var(--gunmetal); 146 } 147 148 .btn-neutral { 149 background: transparent; 150 color: var(--text); 151 border-color: var(--secondary); 152 } 153 154 .btn-neutral:hover { 155 border-color: var(--primary); 156 color: var(--primary); 157 } 158 159 .error { 160 color: var(--accent); 161 text-align: center; 162 padding: 2rem; 163 } 164 165 .loading { 166 text-align: center; 167 color: var(--text); 168 padding: 2rem; 169 } 170 171 @media (max-width: 768px) { 172 h1 { 173 font-size: 2rem; 174 } 175 176 .message { 177 font-size: 1rem; 178 } 179 } 180 `; 181 182 override connectedCallback() { 183 super.connectedCallback(); 184 const params = new URLSearchParams(window.location.search); 185 this.checkoutId = params.get("checkout_id"); 186 this.loading = false; 187 188 // Trigger confetti after a short delay 189 setTimeout(() => this.fireConfetti(), 300); 190 } 191 192 fireConfetti() { 193 if (!window.confetti) return; 194 195 const count = 200; 196 const defaults = { 197 origin: { y: 0.7 }, 198 }; 199 200 const fire = (particleRatio: number, opts: object) => { 201 window.confetti({ 202 ...defaults, 203 ...opts, 204 particleCount: Math.floor(count * particleRatio), 205 }); 206 }; 207 208 fire(0.25, { 209 spread: 26, 210 startVelocity: 55, 211 }); 212 213 fire(0.2, { 214 spread: 60, 215 }); 216 217 fire(0.35, { 218 spread: 100, 219 decay: 0.91, 220 scalar: 0.8, 221 }); 222 223 fire(0.1, { 224 spread: 120, 225 startVelocity: 25, 226 decay: 0.92, 227 scalar: 1.2, 228 }); 229 230 fire(0.1, { 231 spread: 120, 232 startVelocity: 45, 233 }); 234 } 235 236 override render() { 237 if (this.loading) { 238 return html`<div class="loading">Loading...</div>`; 239 } 240 241 if (this.error) { 242 return html`<div class="error">${this.error}</div>`; 243 } 244 245 return html` 246 <div class="success-container"> 247 <div class="success-icon">馃帀</div> 248 <h1>Thanks for purchasing a subscription for thistle!</h1> 249 <p class="message"> 250 251 ${ 252 this.checkoutId 253 ? html` 254 <div class="checkout-id"> 255 Checkout ID: ${this.checkoutId} 256 </div> 257 ` 258 : "" 259 } 260 Go checkout your classes and try recording a lecture! 261 </p> 262 263 <div class="actions"> 264 <a href="/classes" class="btn btn-affirmative"> 265 Lets go!!! 266 </a> 267 </div> 268 </div> 269 `; 270 } 271}