Skip to content

Commit d518ebb

Browse files
committed
Support new contact button and scheduling.
1 parent 83d022f commit d518ebb

8 files changed

Lines changed: 341 additions & 17 deletions

File tree

.eslintrc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,15 @@
4949
"vue/attribute-hyphenation": "off",
5050
"quotes": "off",
5151
"quote-props": "off"
52-
}
52+
},
53+
"overrides": [
54+
{
55+
"files": ["*.ts", "*.tsx"],
56+
"parser": "@typescript-eslint/parser",
57+
"plugins": ["@typescript-eslint"],
58+
"parserOptions": {
59+
"ecmaFeatures": { "jsx": true }
60+
}
61+
}
62+
]
5363
}

docs/guests/index.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ The DevOps Adventures Podcast is your chance to connect with a thriving communit
8383
1. **Your LinkedIn Profile link**
8484
1. **Relevant Topics that fit the above requirements**
8585

86-
import StylizedButton from '@site/src/components/stylizedButton';
86+
import { SchedulingContactButton } from '@site/src/components/contactButton';
8787

88-
<StylizedButton href="mailto:scheduling@adventuresindevops.com">
89-
✉ Contact us
90-
</StylizedButton>
88+
<SchedulingContactButton />

docs/sponsorship/index.mdx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,32 @@ We make it easy for you to stand out. Whether you're a startup breaking into the
2929

3030
We support sponsorships starting at **`$780` per episode**. And for a **4 episode pack** commitment, there's the reduced price of **only `$620` per episode**. This gets you all of the above. If there are questions or desire for more or less, please let us know.
3131

32-
import StylizedButton from '@site/src/components/stylizedButton';
32+
import { SponsorshipContactButton } from '@site/src/components/contactButton';
3333

3434
<div style={{ display: 'flex', width: '100%', justifyContent: 'center' }}>
3535
<div style={{ paddingRight: '1rem' }}>
36-
<StylizedButton href='mailto:"Adventures%20In%20DevOps%20Sponsorships"<sponsorships@adventuresindevops.com>?subject=Podcast%20Sponsorship%20Inquiry'>
36+
<SponsorshipContactButton>
3737
<div style={{ marginBottom: '1rem' }}>Single episode</div>
3838
<div>$780</div>
3939
<div>per episode</div>
40-
</StylizedButton>
40+
</SponsorshipContactButton>
4141
</div>
4242

4343
<div>
44-
<StylizedButton href='mailto:"Adventures%20In%20DevOps%20Sponsorships"<sponsorships@adventuresindevops.com>?subject=Podcast%20Sponsorship%20Inquiry'>
44+
<SponsorshipContactButton>
4545
<div style={{ marginBottom: '1rem' }}>4 episode commitment</div>
4646
<div>$620</div>
4747
<div>per episode</div>
48-
</StylizedButton>
48+
</SponsorshipContactButton>
4949
</div>
5050

5151
</div>
5252

5353
## 📍 Ready to Join the Adventure?
5454
Sponsoring the DevOps Adventures Podcast is your chance to connect with a thriving community, showcase your brand, and drive real impact in the world of DevOps and beyond.
5555

56-
Let's make it happen. [Contact us](mailto:sponsorships@adventuresindevops.com) today to learn more about our sponsorship packages and secure your spot on the next episode!
56+
Let's make it happen. Contact us today to learn more about our sponsorship packages and secure your spot on the next episode!
5757

58-
<StylizedButton href='mailto:"Adventures%20In%20DevOps%20Sponsorships"<sponsorships@adventuresindevops.com>?subject=Podcast%20Sponsorship%20Inquiry'>
58+
<SponsorshipContactButton>
5959
Contact Us
60-
</StylizedButton>
60+
</SponsorshipContactButton>

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
"@fortawesome/free-regular-svg-icons": "^6.7.2",
7979
"@fortawesome/free-solid-svg-icons": "^6.7.2",
8080
"@fortawesome/react-fontawesome": "^0.2.2",
81+
"@typescript-eslint/eslint-plugin": "^5.62.0",
82+
"@typescript-eslint/parser": "^5.62.0",
8183
"aws-sdk": "^2.1692.0",
8284
"axios": "^1.12.2",
8385
"dart-sass": "^1.25.0",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React, { useEffect, useState } from 'react';
2+
import StylizedButton from '@site/src/components/stylizedButton';
3+
4+
function randomHash(): string {
5+
return Math.random().toString(36).substring(2, 8);
6+
}
7+
8+
function ContactButton({ encodedEmail, displayName, subject, children }: {
9+
encodedEmail: string;
10+
displayName: string;
11+
subject: string;
12+
children: React.ReactNode;
13+
}) {
14+
const [href, setHref] = useState('');
15+
16+
useEffect(() => {
17+
const email = atob(encodedEmail);
18+
const [local, domain] = email.split('@');
19+
const taggedEmail = `${local}+${randomHash()}@${domain}`;
20+
const encodedDisplayName = encodeURIComponent(`"${displayName}"`);
21+
const encodedSubject = encodeURIComponent(subject);
22+
setHref(`mailto:${encodedDisplayName}<${taggedEmail}>?subject=${encodedSubject}`);
23+
}, [encodedEmail, displayName, subject]);
24+
25+
if (!href) {
26+
return null;
27+
}
28+
29+
return (
30+
<StylizedButton href={href}>
31+
{children}
32+
</StylizedButton>
33+
);
34+
}
35+
36+
export function SchedulingContactButton() {
37+
return (
38+
<ContactButton
39+
encodedEmail="c2NoZWR1bGluZ0BhZHZlbnR1cmVzaW5kZXZvcHMuY29t"
40+
displayName="Adventures In DevOps Scheduling"
41+
subject="Podcast Guest Request"
42+
>
43+
✉ Contact us
44+
</ContactButton>
45+
);
46+
}
47+
48+
export function SponsorshipContactButton({ children }: { children?: React.ReactNode }) {
49+
return (
50+
<ContactButton
51+
encodedEmail="c3BvbnNvcnNoaXBzQGFkdmVudHVyZXNpbmRldm9wcy5jb20="
52+
displayName="Adventures In DevOps Sponsorships"
53+
subject="Podcast Sponsorship Inquiry"
54+
>
55+
{children || '✉ Contact us'}
56+
</ContactButton>
57+
);
58+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3+
import { faSpinner, faCopy, faCheck } from '@fortawesome/free-solid-svg-icons';
4+
import { SchedulingContactButton } from '@site/src/components/contactButton';
5+
6+
const SALT = 'adv-devops-sched-2026';
7+
const EXPIRY_DAYS = 30;
8+
const SCHEDULE_URL = 'https://oncehub.com/adventures-in-devops';
9+
10+
// Simple sync hash: djb2 variant with salt mixing
11+
function hashCode(input: string): number {
12+
let hash = 5381;
13+
const salted = `${SALT}:${input}`;
14+
for (let i = 0; i < salted.length; i++) {
15+
hash = ((hash << 5) + hash + salted.charCodeAt(i)) | 0;
16+
}
17+
return Math.abs(hash);
18+
}
19+
20+
function daysSinceEpoch(): number {
21+
return Math.floor(Date.now() / (1000 * 60 * 60 * 24));
22+
}
23+
24+
function generateId(): string {
25+
const days = daysSinceEpoch();
26+
const timestamp = days.toString(36);
27+
const random = Math.random().toString(36).substring(2, 5);
28+
const checksum = hashCode(`${timestamp}-${random}`).toString(36).substring(0, 4);
29+
return `${timestamp}-${random}-${checksum}`;
30+
}
31+
32+
function validateId(id: string): { valid: boolean; expired?: boolean } {
33+
const parts = id.split('-');
34+
if (parts.length !== 3) {
35+
return { valid: false };
36+
}
37+
38+
const [timestamp, random, checksum] = parts;
39+
40+
const expected = hashCode(`${timestamp}-${random}`).toString(36).substring(0, 4);
41+
if (checksum !== expected) {
42+
return { valid: false };
43+
}
44+
45+
const days = parseInt(timestamp, 36);
46+
const now = daysSinceEpoch();
47+
if (isNaN(days) || days > now || now - days > EXPIRY_DAYS) {
48+
return { valid: false, expired: true };
49+
}
50+
51+
return { valid: true };
52+
}
53+
54+
export default function ScheduleRedirect() {
55+
const [mode, setMode] = useState<'loading' | 'generate' | 'invalid' | 'expired'>('loading');
56+
const [generatedUrl, setGeneratedUrl] = useState('');
57+
const [copied, setCopied] = useState(false);
58+
59+
useEffect(() => {
60+
const params = new URLSearchParams(window.location.search);
61+
62+
if (params.has('generate')) {
63+
const id = generateId();
64+
const base = `${window.location.origin}/schedule?id=${id}`;
65+
setGeneratedUrl(base);
66+
setMode('generate');
67+
return;
68+
}
69+
70+
const id = params.get('id');
71+
if (id) {
72+
const result = validateId(id);
73+
if (result.valid) {
74+
window.location.replace(SCHEDULE_URL);
75+
return;
76+
}
77+
setMode(result.expired ? 'expired' : 'invalid');
78+
return;
79+
}
80+
81+
setMode('invalid');
82+
}, []);
83+
84+
function copyUrl() {
85+
navigator.clipboard.writeText(generatedUrl);
86+
setCopied(true);
87+
setTimeout(() => setCopied(false), 2000);
88+
}
89+
90+
if (mode === 'generate') {
91+
return (
92+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '300px', gap: '20px' }}>
93+
<h2>Schedule Link Generated</h2>
94+
<p style={{ color: 'var(--ifm-color-secondary-darkest)', fontSize: '14px' }}>
95+
This link expires in {EXPIRY_DAYS} days.
96+
</p>
97+
<div style={{
98+
display: 'flex', alignItems: 'center', gap: '10px',
99+
background: 'var(--ifm-background-surface-color)', padding: '12px 16px',
100+
borderRadius: '8px', border: '1px solid var(--ifm-color-emphasis-300)',
101+
maxWidth: '100%', overflow: 'hidden'
102+
}}>
103+
<code style={{ fontSize: '14px', wordBreak: 'break-all' }}>{generatedUrl}</code>
104+
<button
105+
onClick={copyUrl}
106+
type="button"
107+
style={{
108+
background: 'none', border: 'none', cursor: 'pointer',
109+
color: 'var(--ifm-link-color)', fontSize: '18px', flexShrink: 0
110+
}}
111+
title="Copy to clipboard"
112+
>
113+
<FontAwesomeIcon icon={copied ? faCheck : faCopy} />
114+
</button>
115+
</div>
116+
</div>
117+
);
118+
}
119+
120+
if (mode === 'invalid' || mode === 'expired') {
121+
const message = mode === 'expired'
122+
? 'This scheduling link has expired.'
123+
: 'This scheduling link is not valid.';
124+
125+
return (
126+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '300px', gap: '20px', textAlign: 'center' }}>
127+
<h2>{message}</h2>
128+
<p>
129+
If you&apos;d like to schedule an appearance on the podcast, please reach out to us directly.
130+
</p>
131+
<SchedulingContactButton />
132+
</div>
133+
);
134+
}
135+
136+
// Loading/redirecting state
137+
return (
138+
<div style={{ display: 'flex', justifyContent: 'center', height: '100%', alignItems: 'center', minHeight: '300px' }}>
139+
<FontAwesomeIcon icon={faSpinner} size="5x" spin />
140+
</div>
141+
);
142+
}

src/pages/schedule.mdx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ id: schedule
1010
<meta httpEquiv="X-UA-Compatible" content="ie=edge" />
1111
<meta name="theme-color" content="#2d434d" />
1212
<title>Adventures in DevOps Schedule Appearance</title>
13-
14-
<meta http-equiv="refresh" content="0; URL='https://oncehub.com/adventures-in-devops'" />
1513
</head>
1614

17-
import Spinner from '@site/src/components/spinner';
15+
import ScheduleRedirect from '@site/src/components/scheduleRedirect';
1816

19-
<Spinner />
17+
<ScheduleRedirect />

0 commit comments

Comments
 (0)