History

Merge pull request #739 from RhysSullivan/claude/update-marketing-modal-6MTp4

Rhys Sullivan committed 6 months ago

Commit 8db007b2 parents d021499 cbe8f461 file · +218 −12Browse files

Unified · Split

apps/marketing/src/pages/index.astro+218−12
@@ -268,18 +268,156 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 267 unmodified lines
268 <p268 <p
269 class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1"269 class="text-[14.5px] leading-[1.55] text-ink-2 mb-6 text-pretty flex-1"
270 >270 >
271 We're looking for early adopters to help us develop the271 We're looking for early adopters to give feedback and use the
272 self-hosted product. If you're a company interested in self272 self hosted product. If you're a company interested in self
273 hosting get in touch.273 hosting get in touch.
274 </p>274 </p>
275 <a href="mailto:rhys@executor.sh" class="btn-link self-start"275 <button
276 >Get in touch →</a276 type="button"
277 class="btn-link self-start"
278 data-open-modal="self-host-contact"
279 >Get in touch →</button
277 >280 >
278 </div>281 </div>
279 </div>282 </div>
280 </div>283 </div>
281 </section>284 </section>
282 285
286 {/* ─── SELF-HOST CONTACT MODAL ─── */}
287 <dialog
288 id="self-host-contact"
289 class="self-host-modal"
290 aria-labelledby="self-host-contact-title"
291 >
292 <div class="surface-card p-7 max-w-[520px] w-full">
293 <div class="flex items-start justify-between gap-4 mb-5">
294 <h3
295 id="self-host-contact-title"
296 class="text-[20px] font-semibold tracking-[-0.01em] text-ink"
297 >
298 Get in touch
299 </h3>
300 <button
301 type="button"
302 class="text-ink-3 hover:text-ink transition-colors"
303 data-close-modal
304 aria-label="Close"
305 >
306 <svg
307 width="18"
308 height="18"
309 viewBox="0 0 24 24"
310 fill="none"
311 stroke="currentColor"
312 stroke-width="2"
313 stroke-linecap="round"
314 stroke-linejoin="round"
315 ><line x1="18" y1="6" x2="6" y2="18"></line><line
316 x1="6"
317 y1="6"
318 x2="18"
319 y2="18"></line></svg
320 >
321 </button>
322 </div>
323
324 <p class="text-[13.5px] leading-[1.55] text-ink-2 mb-2">
325 Copy the template, fill in your company, and send it over.
326 </p>
327
328 <div class="relative mb-5">
329 <textarea
330 id="self-host-template"
331 class="w-full text-[14px] leading-[1.55] text-ink bg-surface-2 border border-rule rounded-md p-3 pr-12 font-mono resize-none"
332 rows="3"
333 readonly
334 >Hi Rhys, I'm at {company} and interested in self hosting executor.</textarea
335 >
336 <button
337 type="button"
338 class="copy-btn absolute top-2 right-2 p-1.5 rounded text-ink-3 hover:text-ink hover:bg-surface transition-colors"
339 data-copy-target="self-host-template"
340 aria-label="Copy template to clipboard"
341 >
342 <span class="copy-icons" aria-hidden="true">
343 <svg
344 class="copy-icon-copy"
345 width="14"
346 height="14"
347 viewBox="0 0 24 24"
348 fill="none"
349 stroke="currentColor"
350 stroke-width="2"
351 stroke-linecap="round"
352 stroke-linejoin="round"
353 ><rect x="9" y="9" width="13" height="13" rx="2" ry="2"
354 ></rect><path
355 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
356 ></path></svg
357 >
358 <svg
359 class="copy-icon-check"
360 width="14"
361 height="14"
362 viewBox="0 0 24 24"
363 fill="none"
364 stroke="currentColor"
365 stroke-width="2.5"
366 stroke-linecap="round"
367 stroke-linejoin="round"
368 ><polyline points="20 6 9 17 4 12"></polyline></svg
369 >
370 </span>
371 </button>
372 </div>
373
374 <div
375 class="flex items-center justify-between gap-3 bg-surface-2 border border-rule rounded-md px-3 py-2"
376 >
377 <span class="font-mono text-[14px] text-ink truncate"
378 >rhys@executor.sh</span
379 >
380 <button
381 type="button"
382 class="copy-btn flex items-center gap-1.5 text-[13px] text-ink-2 hover:text-ink transition-colors"
383 data-copy="rhys@executor.sh"
384 aria-label="Copy email to clipboard"
385 >
386 <span class="copy-label">Copy</span>
387 <span class="copy-icons" aria-hidden="true">
388 <svg
389 class="copy-icon-copy"
390 width="14"
391 height="14"
392 viewBox="0 0 24 24"
393 fill="none"
394 stroke="currentColor"
395 stroke-width="2"
396 stroke-linecap="round"
397 stroke-linejoin="round"
398 ><rect x="9" y="9" width="13" height="13" rx="2" ry="2"
399 ></rect><path
400 d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
401 ></path></svg
402 >
403 <svg
404 class="copy-icon-check"
405 width="14"
406 height="14"
407 viewBox="0 0 24 24"
408 fill="none"
409 stroke="currentColor"
410 stroke-width="2.5"
411 stroke-linecap="round"
412 stroke-linejoin="round"
413 ><polyline points="20 6 9 17 4 12"></polyline></svg
414 >
415 </span>
416 </button>
417 </div>
418 </div>
419 </dialog>
420
283 {/* ─── FOOTER ─── */}421 {/* ─── FOOTER ─── */}
284 <footer class="border-t border-rule py-10">422 <footer class="border-t border-rule py-10">
285 <div423 <div
@@ -309,23 +447,91 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo"; · 23 unmodified lines
309</Layout>447</Layout>
310 448
311<script>449<script>
450 const flashCopied = (() => {
451 const timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
452 return (btn: HTMLElement, ok: boolean) => {
453 btn.dataset.copied = ok ? "true" : "false";
454 const existing = timers.get(btn);
455 if (existing) clearTimeout(existing);
456 timers.set(
457 btn,
458 setTimeout(() => {
459 delete btn.dataset.copied;
460 timers.delete(btn);
461 }, 1500),
462 );
463 };
464 })();
465
312 document466 document
313 .querySelectorAll<HTMLButtonElement>("button[data-copy]")467 .querySelectorAll<HTMLButtonElement>("button[data-copy]")
314 .forEach((btn) => {468 .forEach((btn) => {
315 let timer: ReturnType<typeof setTimeout> | null = null;
316
317 btn.addEventListener("click", async () => {469 btn.addEventListener("click", async () => {
318 const text = btn.dataset.copy ?? "";470 const text = btn.dataset.copy ?? "";
319 try {471 try {
320 await navigator.clipboard.writeText(text);472 await navigator.clipboard.writeText(text);
321 btn.dataset.copied = "true";473 flashCopied(btn, true);
322 } catch {474 } catch {
323 btn.dataset.copied = "false";475 flashCopied(btn, false);
324 }476 }
325 if (timer) clearTimeout(timer);477 });
326 timer = setTimeout(() => {478 });
327 delete btn.dataset.copied;479
328 }, 1500);480 document
481 .querySelectorAll<HTMLButtonElement>("button[data-copy-target]")
482 .forEach((btn) => {
483 btn.addEventListener("click", async () => {
484 const id = btn.dataset.copyTarget ?? "";
485 const target = document.getElementById(id) as
486 | HTMLTextAreaElement
487 | HTMLInputElement
488 | null;
489 const text = target?.value ?? "";
490 try {
491 await navigator.clipboard.writeText(text);
492 flashCopied(btn, true);
493 } catch {
494 flashCopied(btn, false);
495 }
496 });
497 });
498
499 document
500 .querySelectorAll<HTMLButtonElement>("button[data-open-modal]")
501 .forEach((btn) => {
502 btn.addEventListener("click", () => {
503 const id = btn.dataset.openModal ?? "";
504 const dialog = document.getElementById(id) as HTMLDialogElement | null;
505 dialog?.showModal();
506 });
507 });
508
509 document
510 .querySelectorAll<HTMLDialogElement>("dialog.self-host-modal")
511 .forEach((dialog) => {
512 dialog
513 .querySelectorAll<HTMLButtonElement>("button[data-close-modal]")
514 .forEach((btn) => {
515 btn.addEventListener("click", () => dialog.close());
516 });
517
518 dialog.addEventListener("click", (event) => {
519 if (event.target === dialog) dialog.close();
329 });520 });
330 });521 });
331</script>522</script>
523
524<style>
525 dialog.self-host-modal {
526 background: transparent;
527 border: none;
528 padding: 0;
529 max-width: 92vw;
530 width: 520px;
531 color: inherit;
532 }
533 dialog.self-host-modal::backdrop {
534 background: rgba(0, 0, 0, 0.5);
535 backdrop-filter: blur(4px);
536 }
537</style>