馃 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}