Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
3656a4b
everything
Pato-desu Dec 18, 2025
09d0d8d
double linking elimination
Pato-desu Dec 18, 2025
2b329f4
Resolve merge conflict: moved volunteer vacancies to join page and de…
Pato-desu Dec 30, 2025
035ecf7
first draft
Pato-desu Jan 3, 2026
fbbdad9
removing redundancy
Pato-desu Jan 5, 2026
dbc2f81
wording and spaces
Pato-desu Jan 5, 2026
4437a30
adding mailersend
Pato-desu Jan 5, 2026
86aa026
reducing spaces more
Pato-desu Jan 5, 2026
4a17fac
rounding up stuff
Pato-desu Jan 5, 2026
7d91bad
link from /press
Pato-desu Jan 6, 2026
30cba2c
partnerships + linking back to /press
Pato-desu Jan 6, 2026
028e47e
Feedback
Pato-desu Jan 6, 2026
4d8bc7a
Merge branch 'main' into Contact-us-page
Pato-desu Jan 6, 2026
1806030
forcing netlify update?
Pato-desu Jan 6, 2026
31ed6ed
fix?
Pato-desu Jan 6, 2026
75363c1
fix? 2
Pato-desu Jan 6, 2026
fb909a6
fix 3
Pato-desu Jan 6, 2026
87ef9a6
changing recipients for testing purposes
Pato-desu Jan 6, 2026
7e7f027
data saving of fields
Pato-desu Jan 6, 2026
5ea47a0
change contact to contact us
Pato-desu Jan 6, 2026
469b271
fixing anon@pauseai.info
Pato-desu Jan 6, 2026
957dc0a
debugging sending errors
Pato-desu Jan 6, 2026
e2b8f3b
fetch
Pato-desu Jan 20, 2026
49b9831
Merge branch 'main' into Contact-us-page
Pato-desu Jan 20, 2026
3a79a4b
pnpm-lock fix?
Pato-desu Jan 20, 2026
f6d35d3
Remove unused mailersend dependency
Pato-desu Jan 20, 2026
42c1f6c
Configure separate email recipients for each contact form
Pato-desu Jan 20, 2026
73bd9fd
Merge branch 'main' of https://github.com/PauseAI/pauseai-website int…
Pato-desu Feb 2, 2026
65832f5
removed general tab
Pato-desu Feb 2, 2026
41dc0cc
added email auto responses
Pato-desu Feb 2, 2026
8af91b9
Specific form clarification
Pato-desu Feb 2, 2026
57de9c3
Partnerships form changes
Pato-desu Feb 2, 2026
605e876
Merge branch 'main' into Contact-us-page
Pato-desu Feb 3, 2026
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
24 changes: 19 additions & 5 deletions src/routes/contact-us/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ async function sendContactEmail(data: {
message: string
type: 'Standard' | 'Media' | 'Partnerships' | 'Feedback'
organization?: string
city_country?: string
}) {
if (!env.MAILERSEND_API_KEY) {
console.error('MAILERSEND_API_KEY is not configured')
Expand All @@ -40,9 +41,13 @@ async function sendContactEmail(data: {
htmlContent += `<p><strong>Organization:</strong> ${data.organization}</p>`
}

if (data.city_country) {
htmlContent += `<p><strong>City, Country:</strong> ${data.city_country}</p>`
}

htmlContent += `<p><strong>Message:</strong></p><p>${data.message.replace(/\n/g, '<br>')}</p>`

const textContent = `Name: ${data.name}\nEmail: ${data.email}\nSubject: ${data.subject}${data.organization ? `\nOrganization: ${data.organization}` : ''}\n\nMessage:\n${data.message}`
const textContent = `Name: ${data.name}\nEmail: ${data.email}\nSubject: ${data.subject}${data.organization ? `\nOrganization: ${data.organization}` : ''}${data.city_country ? `\nCity, Country: ${data.city_country}` : ''}\n\nMessage:\n${data.message}`

// Build the request body for MailerSend API
const emailBody: {
Expand All @@ -55,15 +60,15 @@ async function sendContactEmail(data: {
} = {
from: {
email: 'info@pauseai.info',
name: 'PauseAI Contact Form'
name: `PauseAI ${data.type} Form`
},
to: [
{
email: recipientEmail,
name: 'PauseAI Team'
}
],
subject: `[Contact Form] ${data.subject}`,
subject: `[${data.type} Form]: ${data.subject}`,
html: htmlContent,
text: textContent
}
Expand Down Expand Up @@ -225,17 +230,26 @@ export const actions: Actions = {
const name = data.get('name')?.toString()
const email = data.get('email')?.toString()
const organization = data.get('organization')?.toString()
const subject = data.get('subject')?.toString()
const city_country = data.get('city_country')?.toString()

let subject = data.get('partnership_type')?.toString() || ''
const other_type = data.get('other_partnership_type')?.toString()

if (subject === 'Other' && other_type) {
subject = `Other: ${other_type}`
}

const message = data.get('message')?.toString()

if (!name || !email || !organization || !subject || !message) {
if (!name || !email || !city_country || !subject || !message) {
return fail(400, { message: 'Missing required fields' })
}

const result = await sendContactEmail({
name,
email,
organization,
city_country,
subject,
message,
type: 'Partnerships'
Expand Down
156 changes: 143 additions & 13 deletions src/routes/contact-us/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,64 @@

let formData = {
media: { name: '', email: '', subject: '', organization: '', details: '' },
partnerships: { name: '', email: '', organization: '', subject: '', message: '' },
partnerships: {
name: '',
email: '',
organization: '',
city_country: '',
partnership_type: '',
other_partnership_type: '',
message: ''
},
feedback: { name: '', email: '', subject: '', message: '' }
}

const partnershipOptions = [
'Mobilize grassroots support for PauseAI’s mission',
'Open a chapter in my city/country',
'Organize a public demonstration or protest',
'Support citizen lobbying efforts',
'Invite Pause AI to participate/speak at your local event/meeting',
'Test and refine policy proposals with policymakers',
'Amplify Pause AI campaign and/or proposal',
'Assist with surveys or qualitative data collection',
'Help disseminate research findings to the public',
'Connect Pause AI with experts',
'Share grassroots public sentiment data',
'Exchange volunteers for events or campaigns',
'Pool resources for joint campaigns or event',
'Collaborate on grant applications',
'Co-create educational or advocacy content',
'Mobilize volunteers for emergency response',
'Adapt messaging for local/international contexts',
'Connect with engaged volunteer base',
'Explore general partnership opportunities',
'Other'
]

function countWords(str: string) {
return str.trim().split(/\s+/).length
}

function validatePartnershipForm() {
if (activeTab !== 'partnerships') return true

if (
formData.partnerships.partnership_type === 'Other' &&
countWords(formData.partnerships.other_partnership_type) > 10
) {
toast.error('Other partnership type must be 10 words or less.')
return false
}

const messageWords = countWords(formData.partnerships.message)
if (messageWords > 200) {
toast.error(`Message must be 200 words or less. (Current: ${messageWords} words)`)
return false
}
return true
}

onMount(() => {
const tab = $page.url.searchParams.get('tab')
if (tab === 'media') {
Expand All @@ -45,7 +99,19 @@
localStorage.setItem('contactFormData', JSON.stringify(formData))
}

function handleEnhance() {
function handleEnhance({ cancel }: { cancel: () => void }) {
if (activeTab === 'partnerships') {
// Manually validate because we can't easily use the form state inside the enhancer
// without potentially stale data if we just used `formData` variable.
// But `formData` variable IS bound to inputs, so it should be fine.
// However, `data` from the argument contains the actual FormData being submitted.

if (!validatePartnershipForm()) {
cancel()
return
}
}

loading = true
return async ({
result,
Expand All @@ -66,7 +132,9 @@
name: '',
email: '',
organization: '',
subject: '',
city_country: '',
partnership_type: '',
other_partnership_type: '',
message: ''
}
} else if (activeTab === 'feedback') {
Expand Down Expand Up @@ -119,9 +187,8 @@
{#if activeTab === 'partnerships'}
<section id="partnerships-contact">
<p class="tab-intro">
Interested in collaborating? Read about our <Link href="/partnerships"
>partnership opportunities</Link
>.
Ready to collaborate with PauseAI's network to build the momentum required to drive
meaningful change in AI policy? We would love to hear from you.
</p>
<form method="POST" action="?/partnerships" use:enhance={handleEnhance}>
<div class="field">
Expand Down Expand Up @@ -149,27 +216,57 @@
type="text"
id="part-org"
name="organization"
required
placeholder="Organization"
placeholder="Organization (Optional)"
bind:value={formData.partnerships.organization}
/>
</div>
<div class="field">
<input
type="text"
id="part-subject"
name="subject"
id="part-city"
name="city_country"
required
placeholder="Subject"
bind:value={formData.partnerships.subject}
placeholder="City, Country"
bind:value={formData.partnerships.city_country}
/>
</div>
<div class="field">
<label for="part-type" class="field-label"
>How would you like to partner with us? *</label
>
<select
id="part-type"
name="partnership_type"
required
bind:value={formData.partnerships.partnership_type}
>
<option value="" disabled selected>Select an option</option>
{#each partnershipOptions as option}
<option value={option}>{option}</option>
{/each}
</select>
</div>

{#if formData.partnerships.partnership_type === 'Other'}
<div class="field">
<input
type="text"
id="part-other"
name="other_partnership_type"
required
placeholder="Enter details (max 10 words)"
bind:value={formData.partnerships.other_partnership_type}
/>
</div>
{/if}

<div class="field">
<label for="part-message" class="field-label">Details (max 200 words) *</label>
<textarea
id="part-message"
name="message"
required
placeholder="How would you like to partner with us?"
placeholder="Provide additional details to how you would like to partner with us, particularly if related to a time sensitive matter."
bind:value={formData.partnerships.message}
></textarea>
</div>
Expand Down Expand Up @@ -456,4 +553,37 @@
gap: 0.5rem;
}
}

select {
width: 100%;
padding: 0.8rem 1.2rem;
border: 1px solid var(--brand-subtle);
border-radius: 20px;
background-color: var(--bg);
color: var(--text);
font-family: var(--font-body);
font-size: 1rem;
font-weight: 300;
box-sizing: border-box;
display: block;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 1em;
}

select:focus {
outline: 2px solid var(--brand);
border-color: transparent;
}

.field-label {
margin-bottom: 0.5rem;
margin-left: 0.5rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--text);
opacity: 0.9;
}
</style>
Loading