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
4 changes: 4 additions & 0 deletions apps/web/app/api/contact/route.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createContactRoute } from '@profullstack/stack/email';
import { contactGuard } from '@/lib/contact-guard';

export const POST = createContactRoute({
// Requires a token minted when the form rendered. Runs before field
// validation, so a bot never learns which fields the route wants.
guard: contactGuard ?? undefined,
from: 'MarkSyncr Contact <noreply@marksyncr.com>',
to: 'support@marksyncr.com',
requiredFields: ['name', 'email', 'subject', 'message'],
Expand Down
311 changes: 311 additions & 0 deletions apps/web/app/contact/ContactPageClient.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
'use client';

import { useState } from 'react';
import Link from 'next/link';
import Image from 'next/image';

export default function ContactPageClient({ token, tokenName, honeypotName }) {
// Honeypot. Nothing visible sets this, so anything in it came from a bot.
const [honeypot, setHoneypot] = useState('');
const [formData, setFormData] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [status, setStatus] = useState({ type: '', message: '' });
const [isSubmitting, setIsSubmitting] = useState(false);

const handleChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};

const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setStatus({ type: '', message: '' });

try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...formData,
...(tokenName && token ? { [tokenName]: token } : {}),
...(honeypotName ? { [honeypotName]: honeypot } : {}),
}),
});

const data = await response.json();

if (response.ok) {
setStatus({
type: 'success',
message: 'Thank you! Your message has been sent successfully.',
});
setFormData({ name: '', email: '', subject: '', message: '' });
} else {
setStatus({
type: 'error',
message: data.error || 'Failed to send message. Please try again.',
});
}
} catch (error) {
setStatus({
type: 'error',
message: 'An unexpected error occurred. Please try again later.',
});
} finally {
setIsSubmitting(false);
}
};

return (
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
{/* Navigation */}
<nav className="border-b border-slate-200 bg-white/80 backdrop-blur-sm">
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
<Link href="/" className="flex items-center">
<Image
src="/logo.svg"
alt="MarkSyncr"
width={175}
height={40}
className="h-10 w-auto"
/>
</Link>
<div className="flex items-center space-x-4">
<Link href="/login" className="text-sm text-slate-600 hover:text-slate-900">
Log in
</Link>
<Link
href="/signup"
className="rounded-lg bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700"
>
Get Started
</Link>
</div>
</div>
</nav>

{/* Main Content */}
<main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
<div className="mx-auto max-w-2xl">
<div className="text-center">
<h1 className="text-4xl font-bold tracking-tight text-slate-900">Contact Us</h1>
<p className="mt-4 text-lg text-slate-600">
Have a question or need help? We'd love to hear from you.
</p>
</div>

{/* Contact Form */}
<form onSubmit={handleSubmit} className="mt-12 space-y-6">
{/* Honeypot. Off-canvas rather than display:none, because some
bots skip fields they can tell are not rendered. The route had
one configured all along but never rendered it. */}
{honeypotName && (
<div aria-hidden="true" className="absolute -left-[9999px] h-px w-px overflow-hidden">
<label>
Website
<input
type="text"
name={honeypotName}
value={honeypot}
onChange={(e) => setHoneypot(e.target.value)}
tabIndex={-1}
autoComplete="off"
/>
</label>
</div>
)}
{status.message && (
<div
className={`rounded-lg p-4 ${
status.type === 'success'
? 'bg-green-50 text-green-800'
: 'bg-red-50 text-red-800'
}`}
>
{status.message}
</div>
)}

<div>
<label htmlFor="name" className="block text-sm font-medium text-slate-700">
Name
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
className="mt-1 block w-full rounded-lg border border-slate-300 px-4 py-2 text-slate-900 placeholder-slate-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
placeholder="Your name"
/>
</div>

<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700">
Email
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
required
className="mt-1 block w-full rounded-lg border border-slate-300 px-4 py-2 text-slate-900 placeholder-slate-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
placeholder="you@example.com"
/>
</div>

<div>
<label htmlFor="subject" className="block text-sm font-medium text-slate-700">
Subject
</label>
<select
id="subject"
name="subject"
value={formData.subject}
onChange={handleChange}
required
className="mt-1 block w-full rounded-lg border border-slate-300 px-4 py-2 text-slate-900 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
>
<option value="">Select a subject</option>
<option value="General Inquiry">General Inquiry</option>
<option value="Technical Support">Technical Support</option>
<option value="Billing Question">Billing Question</option>
<option value="Feature Request">Feature Request</option>
<option value="Bug Report">Bug Report</option>
<option value="Partnership">Partnership</option>
<option value="Other">Other</option>
</select>
</div>

<div>
<label htmlFor="message" className="block text-sm font-medium text-slate-700">
Message
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
required
rows={6}
className="mt-1 block w-full rounded-lg border border-slate-300 px-4 py-2 text-slate-900 placeholder-slate-400 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
placeholder="How can we help you?"
/>
</div>

<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-lg bg-primary-600 px-4 py-3 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>

{/* Alternative Contact Methods */}
<div className="mt-16 border-t border-slate-200 pt-12">
<h2 className="text-center text-lg font-semibold text-slate-900">
Other Ways to Reach Us
</h2>
<div className="mt-8 grid gap-8 sm:grid-cols-3">
<div className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
<svg
className="h-6 w-6 text-primary-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<h3 className="mt-4 text-sm font-medium text-slate-900">Email</h3>
<p className="mt-1 text-sm text-slate-600">
<a
href="mailto:support@marksyncr.com"
className="text-primary-600 hover:text-primary-700"
>
support@marksyncr.com
</a>
</p>
</div>

<div className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
<svg className="h-6 w-6 text-primary-600" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
</svg>
</div>
<h3 className="mt-4 text-sm font-medium text-slate-900">Discord</h3>
<p className="mt-1 text-sm text-slate-600">
<a
href="https://discord.gg/U7dEXfBA3s"
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 hover:text-primary-700"
>
Join our community
</a>
</p>
</div>

<div className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-lg bg-primary-100">
<svg className="h-6 w-6 text-primary-600" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
</div>
<h3 className="mt-4 text-sm font-medium text-slate-900">GitHub</h3>
<p className="mt-1 text-sm text-slate-600">
<a
href="https://github.com/profullstack/marksyncr.com"
target="_blank"
rel="noopener noreferrer"
className="text-primary-600 hover:text-primary-700"
>
Report issues
</a>
</p>
</div>
</div>
</div>
</div>
</main>

{/* Footer */}
<footer className="border-t border-slate-200 bg-white py-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col items-center justify-between md:flex-row">
<p className="text-sm text-slate-500">
© {new Date().getFullYear()} MarkSyncr. All rights reserved.
</p>
<div className="mt-4 flex space-x-6 md:mt-0">
<Link href="/privacy" className="text-sm text-slate-600 hover:text-slate-900">
Privacy
</Link>
<Link href="/terms" className="text-sm text-slate-600 hover:text-slate-900">
Terms
</Link>
</div>
</div>
</div>
</footer>
</div>
);
}
Loading
Loading