Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/marketing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@executor-js/plugin-google-discovery": "workspace:*",
"@executor-js/plugin-graphql": "workspace:*",
"@executor-js/plugin-openapi": "workspace:*",
"@executor-js/react": "workspace:*",
"@executor-js/sdk": "workspace:*",
"@tailwindcss/vite": "^4.2.2",
"astro": "^6.1.3",
Expand Down
104 changes: 104 additions & 0 deletions apps/marketing/src/components/self-host-contact-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Button } from "@executor-js/react/components/button";
import { CopyButton } from "@executor-js/react/components/copy-button";
import {
Dialog,
DialogClose,
DialogContent,
DialogTitle,
DialogTrigger,
} from "@executor-js/react/components/dialog";
const TEMPLATE = "Hi Rhys, I'm at {company} and interested in self hosting executor.";
const EMAIL = "rhys@executor.sh";

export function SelfHostContactModal() {
return (
<Dialog>
<DialogTrigger asChild>
<Button
variant="link"
className="btn-link self-start h-auto p-0 text-current hover:no-underline"
>
Get in touch →
</Button>
</DialogTrigger>
<DialogContent
showCloseButton={false}
className="surface-card gap-0 border-0 p-6 sm:max-w-[520px] sm:p-7"
style={{
background: "var(--color-surface)",
color: "var(--color-ink)",
}}
>
<div className="mb-4 flex items-start justify-between gap-4">
<DialogTitle className="text-[20px] font-semibold tracking-[-0.01em]">
Get in touch
</DialogTitle>
<DialogClose asChild>
<Button
variant="ghost"
size="icon-sm"
aria-label="Close"
style={{ color: "var(--color-ink-3)" }}
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</Button>
</DialogClose>
</div>

<p className="mb-3 text-[13.5px] leading-[1.55]" style={{ color: "var(--color-ink-2)" }}>
Copy the template, fill in your company, and send it to me.
</p>

<div
className="mb-4 rounded-md border p-3"
style={{
background: "var(--color-surface-2)",
borderColor: "var(--color-rule)",
}}
>
<p
className="mb-2 font-mono text-[14px] leading-[1.55]"
style={{ color: "var(--color-ink)" }}
>
{TEMPLATE}
</p>
<div className="flex justify-end">
<CopyButton value={TEMPLATE} label="Copy" />
</div>
</div>

<div
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2"
style={{
background: "var(--color-surface-2)",
borderColor: "var(--color-rule)",
}}
>
<a
href={`mailto:${EMAIL}`}
className="truncate font-mono text-[14px] hover:underline"
style={{ color: "var(--color-ink)" }}
>
{EMAIL}
</a>
<CopyButton value={EMAIL} label="Copy" />
</div>
</DialogContent>
</Dialog>
);
}

export default SelfHostContactModal;
227 changes: 10 additions & 217 deletions apps/marketing/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import Layout from "../layouts/Layout.astro";
import { AnimatedBeamDemo } from "../components/animated-beam-demo";
import { SelfHostContactModal } from "../components/self-host-contact-modal";
---

<Layout>
Expand Down Expand Up @@ -272,152 +273,12 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo";
self hosted product. If you're a company interested in self
hosting get in touch.
</p>
<button
type="button"
class="btn-link self-start"
data-open-modal="self-host-contact"
>Get in touch →</button
>
<SelfHostContactModal client:load />
</div>
</div>
</div>
</section>

{/* ─── SELF-HOST CONTACT MODAL ─── */}
<dialog
id="self-host-contact"
class="self-host-modal"
aria-labelledby="self-host-contact-title"
>
<div class="surface-card p-7 max-w-[520px] w-full">
<div class="flex items-start justify-between gap-4 mb-5">
<h3
id="self-host-contact-title"
class="text-[20px] font-semibold tracking-[-0.01em] text-ink"
>
Get in touch
</h3>
<button
type="button"
class="text-ink-3 hover:text-ink transition-colors"
data-close-modal
aria-label="Close"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><line x1="18" y1="6" x2="6" y2="18"></line><line
x1="6"
y1="6"
x2="18"
y2="18"></line></svg
>
</button>
</div>

<p class="text-[13.5px] leading-[1.55] text-ink-2 mb-2">
Copy the template, fill in your company, and send it over.
</p>

<div class="relative mb-5">
<textarea
id="self-host-template"
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"
rows="3"
readonly
>Hi Rhys, I'm at {company} and interested in self hosting executor.</textarea
>
<button
type="button"
class="copy-btn absolute top-2 right-2 p-1.5 rounded text-ink-3 hover:text-ink hover:bg-surface transition-colors"
data-copy-target="self-host-template"
aria-label="Copy template to clipboard"
>
<span class="copy-icons" aria-hidden="true">
<svg
class="copy-icon-copy"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="9" y="9" width="13" height="13" rx="2" ry="2"
></rect><path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></path></svg
>
<svg
class="copy-icon-check"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
><polyline points="20 6 9 17 4 12"></polyline></svg
>
</span>
</button>
</div>

<div
class="flex items-center justify-between gap-3 bg-surface-2 border border-rule rounded-md px-3 py-2"
>
<span class="font-mono text-[14px] text-ink truncate"
>rhys@executor.sh</span
>
<button
type="button"
class="copy-btn flex items-center gap-1.5 text-[13px] text-ink-2 hover:text-ink transition-colors"
data-copy="rhys@executor.sh"
aria-label="Copy email to clipboard"
>
<span class="copy-label">Copy</span>
<span class="copy-icons" aria-hidden="true">
<svg
class="copy-icon-copy"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
><rect x="9" y="9" width="13" height="13" rx="2" ry="2"
></rect><path
d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"
></path></svg
>
<svg
class="copy-icon-check"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
><polyline points="20 6 9 17 4 12"></polyline></svg
>
</span>
</button>
</div>
</div>
</dialog>

{/* ─── FOOTER ─── */}
<footer class="border-t border-rule py-10">
<div
Expand Down Expand Up @@ -447,91 +308,23 @@ import { AnimatedBeamDemo } from "../components/animated-beam-demo";
</Layout>

<script>
const flashCopied = (() => {
const timers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
return (btn: HTMLElement, ok: boolean) => {
btn.dataset.copied = ok ? "true" : "false";
const existing = timers.get(btn);
if (existing) clearTimeout(existing);
timers.set(
btn,
setTimeout(() => {
delete btn.dataset.copied;
timers.delete(btn);
}, 1500),
);
};
})();

document
.querySelectorAll<HTMLButtonElement>("button[data-copy]")
.forEach((btn) => {
btn.addEventListener("click", async () => {
const text = btn.dataset.copy ?? "";
try {
await navigator.clipboard.writeText(text);
flashCopied(btn, true);
} catch {
flashCopied(btn, false);
}
});
});
let timer: ReturnType<typeof setTimeout> | null = null;

document
.querySelectorAll<HTMLButtonElement>("button[data-copy-target]")
.forEach((btn) => {
btn.addEventListener("click", async () => {
const id = btn.dataset.copyTarget ?? "";
const target = document.getElementById(id) as
| HTMLTextAreaElement
| HTMLInputElement
| null;
const text = target?.value ?? "";
const text = btn.dataset.copy ?? "";
try {
await navigator.clipboard.writeText(text);
flashCopied(btn, true);
btn.dataset.copied = "true";
} catch {
flashCopied(btn, false);
btn.dataset.copied = "false";
}
});
});

document
.querySelectorAll<HTMLButtonElement>("button[data-open-modal]")
.forEach((btn) => {
btn.addEventListener("click", () => {
const id = btn.dataset.openModal ?? "";
const dialog = document.getElementById(id) as HTMLDialogElement | null;
dialog?.showModal();
});
});

document
.querySelectorAll<HTMLDialogElement>("dialog.self-host-modal")
.forEach((dialog) => {
dialog
.querySelectorAll<HTMLButtonElement>("button[data-close-modal]")
.forEach((btn) => {
btn.addEventListener("click", () => dialog.close());
});

dialog.addEventListener("click", (event) => {
if (event.target === dialog) dialog.close();
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
delete btn.dataset.copied;
}, 1500);
});
});
</script>

<style>
dialog.self-host-modal {
background: transparent;
border: none;
padding: 0;
max-width: 92vw;
width: 520px;
color: inherit;
}
dialog.self-host-modal::backdrop {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
</style>
3 changes: 3 additions & 0 deletions apps/marketing/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

@source "../**/*.astro";
@source "../**/*.tsx";
@source "../../../../packages/react/src/components/dialog.tsx";
@source "../../../../packages/react/src/components/copy-button.tsx";
@source "../../../../packages/react/src/components/button.tsx";

@theme {
--font-sans: "Geist", ui-sans-serif, system-ui, sans-serif;
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading