cloudlfare

This commit is contained in:
will
2026-04-02 23:29:59 +01:00
parent 746e51370d
commit c76f66c0fd
6 changed files with 154 additions and 16 deletions

View File

@@ -2,5 +2,5 @@ GITEA_URL=https://git.williammarch.xyz
GITEA_USERNAME=m0dus GITEA_USERNAME=m0dus
NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAACzsXdlmXw00QAai NEXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAACzsqWrzBq00jrzS
TURNSTILE_SECRET_KEY=0x4AAAAAACzsXeYTwCALD_kLJqaZESctCUo TURNSTILE_SECRET_KEY=0x4AAAAAACzsqSwNTHM_tj3EG-q_Nwlxtm4

View File

@@ -89,7 +89,7 @@ export async function POST(req: Request) {
// Send email via local sendmail (Postfix/Sendmail/Exim) // Send email via local sendmail (Postfix/Sendmail/Exim)
await transporter.sendMail({ await transporter.sendMail({
from: 'William March <contact@williammarch.xyz>', from: 'William March <contact@williammarch.xyz>',
to: 'your-real-inbox@example.com', // TODO: change to your inbox to: 'qemuguest@protonmail.com',
replyTo: email, replyTo: email,
subject: `New message from ${name} via williammarch.xyz`, subject: `New message from ${name} via williammarch.xyz`,
text: `From: ${name} <${email}>\n\n${message}`, text: `From: ${name} <${email}>\n\n${message}`,

View File

@@ -32,7 +32,10 @@ export async function GET(req: NextRequest) {
} }
const md = await res.text(); const md = await res.text();
const html = marked.parse(md || '');
// marked.parse can return string | Promise<string>,
// so we call it in async mode and await the result.
const html = await marked.parse(md || '', { async: true });
return new NextResponse(html, { return new NextResponse(html, {
status: 200, status: 200,

View File

@@ -31,6 +31,7 @@ export default function Contact() {
const [turnstileToken, setTurnstileToken] = useState(''); const [turnstileToken, setTurnstileToken] = useState('');
const widgetIdRef = useRef<string | null>(null); const widgetIdRef = useRef<string | null>(null);
const turnstileRef = useRef<HTMLDivElement | null>(null); const turnstileRef = useRef<HTMLDivElement | null>(null);
const startedAtRef = useRef<number>(Date.now());
useEffect(() => { useEffect(() => {
if (!TURNSTILE_SITE_KEY) { if (!TURNSTILE_SITE_KEY) {
@@ -48,6 +49,7 @@ export default function Contact() {
theme: 'dark', theme: 'dark',
callback: token => { callback: token => {
setTurnstileToken(token); setTurnstileToken(token);
setErrorMessage('');
}, },
'expired-callback': () => { 'expired-callback': () => {
setTurnstileToken(''); setTurnstileToken('');
@@ -85,7 +87,7 @@ export default function Contact() {
name: String(fd.get('name') || '').trim(), name: String(fd.get('name') || '').trim(),
email: String(fd.get('email') || '').trim(), email: String(fd.get('email') || '').trim(),
message: String(fd.get('message') || '').trim(), message: String(fd.get('message') || '').trim(),
company: String(fd.get('company') || '').trim(), // honeypot company: String(fd.get('company') || '').trim(),
turnstileToken, turnstileToken,
formStartedAt: String(fd.get('formStartedAt') || ''), formStartedAt: String(fd.get('formStartedAt') || ''),
}; };
@@ -117,7 +119,9 @@ export default function Contact() {
setStatus('sent'); setStatus('sent');
form.reset(); form.reset();
startedAtRef.current = Date.now();
setTurnstileToken(''); setTurnstileToken('');
if (window.turnstile && widgetIdRef.current) { if (window.turnstile && widgetIdRef.current) {
window.turnstile.reset(widgetIdRef.current); window.turnstile.reset(widgetIdRef.current);
} }
@@ -126,10 +130,11 @@ export default function Contact() {
setErrorMessage( setErrorMessage(
err instanceof Error ? err.message : 'Something went wrong.', err instanceof Error ? err.message : 'Something went wrong.',
); );
} finally {
if (status !== 'sent') { if (window.turnstile && widgetIdRef.current) {
setStatus(prev => (prev === 'sent' ? 'sent' : 'idle')); window.turnstile.reset(widgetIdRef.current);
} }
setTurnstileToken('');
} }
} }
@@ -141,6 +146,7 @@ export default function Contact() {
> >
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<span className="label">Get in touch</span> <span className="label">Get in touch</span>
<h2 <h2
id="contact-heading" id="contact-heading"
className="text-[clamp(1.8rem,4vw,2.5rem)] font-black tracking-tight text-white mb-3" className="text-[clamp(1.8rem,4vw,2.5rem)] font-black tracking-tight text-white mb-3"
@@ -153,17 +159,84 @@ export default function Contact() {
</p> </p>
<div className="grid md:grid-cols-[280px_1fr] gap-6"> <div className="grid md:grid-cols-[280px_1fr] gap-6">
{/* Left side contact cards */}
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* email + gitea blocks unchanged */} <a
{/* ... */} href={`mailto:${SITE.email}`}
className="glass rounded-xl p-4 flex items-center gap-3 group hover:border-white/[0.14]
hover:-translate-x-[-4px] transition-all duration-150"
>
<div
className="w-8 h-8 rounded-lg bg-white/[0.05] flex items-center justify-center flex-shrink-0"
aria-hidden="true"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.8"
>
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg>
</div>
<div className="min-w-0">
<p className="text-[10px] text-white/25 uppercase tracking-wider">
Email
</p>
<p className="text-xs font-medium text-white/55 truncate group-hover:text-white/80 transition-colors">
{SITE.email}
</p>
</div>
</a>
<a
href={`${GITEA_URL}/${GITEA_USERNAME}`}
target="_blank"
rel="noopener noreferrer"
className="glass rounded-xl p-4 flex items-center gap-3 group hover:border-white/[0.14]
hover:translate-x-1 transition-all duration-150"
>
<div
className="w-8 h-8 rounded-lg bg-white/[0.05] flex items-center justify-center flex-shrink-0"
aria-hidden="true"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="rgba(255,255,255,0.5)"
strokeWidth="1.8"
>
<path d="M15 22v-4a4.8 4.8 0 0 0-1-3.2c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4" />
<path d="M9 18c-4.51 2-5-2-7-2" />
</svg>
</div>
<div className="min-w-0">
<p className="text-[10px] text-white/25 uppercase tracking-wider">
Gitea
</p>
<p className="text-xs font-medium text-white/55 truncate group-hover:text-white/80 transition-colors">
{GITEA_URL.replace(/^https?:\/\//, '')}/{GITEA_USERNAME}
</p>
</div>
</a>
</div> </div>
{/* Form */}
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="glass rounded-xl p-6 flex flex-col gap-4" className="glass rounded-xl p-6 flex flex-col gap-4 relative"
noValidate noValidate
> >
<input type="hidden" name="formStartedAt" value={String(Date.now())} /> <input
type="hidden"
name="formStartedAt"
value={String(startedAtRef.current)}
/>
{/* Honeypot */} {/* Honeypot */}
<div <div
@@ -180,8 +253,54 @@ export default function Contact() {
/> />
</div> </div>
{/* fields unchanged */} <div className="grid sm:grid-cols-2 gap-4">
{/* ... */} <label className="flex flex-col gap-1.5">
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/30">
Name
</span>
<input
name="name"
type="text"
required
placeholder="Your name"
autoComplete="name"
className="bg-white/[0.03] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm
text-white placeholder-white/20 outline-none transition-colors
focus:border-white/[0.2] focus:ring-1 focus:ring-white/[0.1]"
/>
</label>
<label className="flex flex-col gap-1.5">
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/30">
Email
</span>
<input
name="email"
type="email"
required
placeholder="you@example.com"
autoComplete="email"
className="bg-white/[0.03] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm
text-white placeholder-white/20 outline-none transition-colors
focus:border-white/[0.2] focus:ring-1 focus:ring-white/[0.1]"
/>
</label>
</div>
<label className="flex flex-col gap-1.5">
<span className="text-[10px] font-semibold uppercase tracking-wider text-white/30">
Message
</span>
<textarea
name="message"
required
placeholder="Whats on your mind?"
rows={5}
className="bg-white/[0.03] border border-white/[0.08] rounded-lg px-3 py-2.5 text-sm
text-white placeholder-white/20 outline-none resize-none transition-colors
focus:border-white/[0.2] focus:ring-1 focus:ring-white/[0.1]"
/>
</label>
<div> <div>
{TURNSTILE_SITE_KEY ? ( {TURNSTILE_SITE_KEY ? (
@@ -199,7 +318,11 @@ export default function Contact() {
disabled={status === 'loading'} disabled={status === 'loading'}
className="btn btn-primary disabled:opacity-60 disabled:cursor-not-allowed" className="btn btn-primary disabled:opacity-60 disabled:cursor-not-allowed"
> >
{status === 'loading' ? 'Sending…' : status === 'sent' ? 'Message sent' : 'Send message'} {status === 'loading'
? 'Sending…'
: status === 'sent'
? 'Message sent'
: 'Send message'}
</button> </button>
{status === 'sent' && ( {status === 'sent' && (
@@ -208,7 +331,7 @@ export default function Contact() {
</p> </p>
)} )}
{errorMessage && ( {status === 'error' && errorMessage && (
<p className="text-xs text-red-300/80">{errorMessage}</p> <p className="text-xs text-red-300/80">{errorMessage}</p>
)} )}
</div> </div>

11
package-lock.json generated
View File

@@ -18,6 +18,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.11",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10", "autoprefixer": "^10",
@@ -321,6 +322,16 @@
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/nodemailer": {
"version": "7.0.11",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz",
"integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.15", "version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",

View File

@@ -19,6 +19,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
"@types/nodemailer": "^7.0.11",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10", "autoprefixer": "^10",