feat(standalone): demo follow-ups — waitlist, analytics, move-to-local, file tree, load progress
- Waitlist signup form on the home page (between Projects and Tools), cross-posting to the landing site's /api/waitlist (CORS + OPTIONS added there, gated by WAITLIST_ALLOWED_ORIGINS). - Version badge gains a pcbjam.com landing-page link (VITE_LANDING_URL). - Optional Plausible analytics from VITE_PLAUSIBLE_DOMAIN (off by default); wired into build-demo + both deploy workflows via vars.PLAUSIBLE_DOMAIN. - Opening a read-only gallery project auto-"moves to local": it forks into a writable browser-local project at the same slug, shadowing the gallery row. - Project + local-folder views use the same iconed tool launcher (ToolGrid) and a navigable directory FileTree instead of a flat file list. - Boot overlay shows real wasm download progress (Module.instantiateWasm + streaming byte counter) and a "taking too long" state after 60s. - Home Libraries section lists the active libs source (the read-only R2/CDN set in the demo) with a name filter; useLibs now reads libsSourceConfig. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPJYx4urFVhdMjEmYfMCMD
This commit is contained in:
parent
6c6f889ef5
commit
39cb04b127
22 changed files with 830 additions and 258 deletions
|
|
@ -4,7 +4,12 @@ import { Resend } from 'resend';
|
|||
// and RESEND_SEGMENT_ID are `string | undefined`; WAITLIST_FROM_EMAIL has a
|
||||
// schema default so it's always a `string`. The Vercel adapter reads these from
|
||||
// process.env at runtime — never inlined.
|
||||
import { RESEND_API_KEY, RESEND_SEGMENT_ID, WAITLIST_FROM_EMAIL } from 'astro:env/server';
|
||||
import {
|
||||
RESEND_API_KEY,
|
||||
RESEND_SEGMENT_ID,
|
||||
WAITLIST_FROM_EMAIL,
|
||||
WAITLIST_ALLOWED_ORIGINS,
|
||||
} from 'astro:env/server';
|
||||
|
||||
// Opt this single route into on-demand (serverless) rendering. The rest of the
|
||||
// site stays static; Vercel emits exactly one function for /api/waitlist.
|
||||
|
|
@ -12,10 +17,36 @@ export const prerender = false;
|
|||
|
||||
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||
|
||||
function json(status: number, body: Record<string, unknown>) {
|
||||
/**
|
||||
* CORS headers when the request Origin is in the configured allowlist — lets the
|
||||
* static demo (demo.pcbjam.com, no backend of its own) cross-post the form. A
|
||||
* same-origin submit sends no Origin and gets no CORS headers (it doesn't need
|
||||
* them). The JSON content-type triggers a preflight, hence the OPTIONS handler.
|
||||
*/
|
||||
function corsHeaders(request: Request): Record<string, string> {
|
||||
const origin = request.headers.get('origin');
|
||||
if (!origin) return {};
|
||||
const allowed = WAITLIST_ALLOWED_ORIGINS.split(',')
|
||||
.map((o) => o.trim())
|
||||
.filter(Boolean);
|
||||
if (!allowed.includes(origin)) return {};
|
||||
return {
|
||||
'access-control-allow-origin': origin,
|
||||
'access-control-allow-methods': 'POST, OPTIONS',
|
||||
'access-control-allow-headers': 'content-type',
|
||||
'access-control-max-age': '86400',
|
||||
vary: 'Origin',
|
||||
};
|
||||
}
|
||||
|
||||
function json(
|
||||
status: number,
|
||||
body: Record<string, unknown>,
|
||||
extra: Record<string, string> = {},
|
||||
) {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
headers: { 'content-type': 'application/json', ...extra },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +58,11 @@ function redirect(status: 'ok' | 'error') {
|
|||
});
|
||||
}
|
||||
|
||||
export const OPTIONS: APIRoute = ({ request }) =>
|
||||
new Response(null, { status: 204, headers: corsHeaders(request) });
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const cors = corsHeaders(request);
|
||||
const ct = request.headers.get('content-type') ?? '';
|
||||
const wantsJson = ct.includes('application/json');
|
||||
|
||||
|
|
@ -37,7 +72,7 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
? await request.json()
|
||||
: Object.fromEntries(await request.formData());
|
||||
} catch {
|
||||
return wantsJson ? json(400, { ok: false, error: 'bad_request' }) : redirect('error');
|
||||
return wantsJson ? json(400, { ok: false, error: 'bad_request' }, cors) : redirect('error');
|
||||
}
|
||||
|
||||
const email = String(data.email ?? '').trim().toLowerCase();
|
||||
|
|
@ -45,16 +80,16 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
const source = String(data.source ?? 'unknown');
|
||||
|
||||
// Bot caught by honeypot: silently "succeed" so we don't tip them off.
|
||||
if (honeypot) return wantsJson ? json(200, { ok: true }) : redirect('ok');
|
||||
if (honeypot) return wantsJson ? json(200, { ok: true }, cors) : redirect('ok');
|
||||
|
||||
if (!EMAIL_RE.test(email) || email.length > 254) {
|
||||
return wantsJson ? json(400, { ok: false, error: 'invalid_email' }) : redirect('error');
|
||||
return wantsJson ? json(400, { ok: false, error: 'invalid_email' }, cors) : redirect('error');
|
||||
}
|
||||
|
||||
// No key configured (e.g. local dev without secrets): accept + log, don't 500.
|
||||
if (!RESEND_API_KEY) {
|
||||
console.warn(`[waitlist] RESEND_API_KEY not set — skipping send. email=${email} source=${source}`);
|
||||
return wantsJson ? json(200, { ok: true }) : redirect('ok');
|
||||
return wantsJson ? json(200, { ok: true }, cors) : redirect('ok');
|
||||
}
|
||||
|
||||
const resend = new Resend(RESEND_API_KEY);
|
||||
|
|
@ -98,14 +133,14 @@ export const POST: APIRoute = async ({ request }) => {
|
|||
// rejected it (bad key, unverified domain, invalid from-address, …).
|
||||
if (sendError) {
|
||||
console.error('[waitlist] emails.send failed', sendError);
|
||||
return wantsJson ? json(502, { ok: false, error: 'send_failed' }) : redirect('error');
|
||||
return wantsJson ? json(502, { ok: false, error: 'send_failed' }, cors) : redirect('error');
|
||||
}
|
||||
|
||||
return wantsJson ? json(200, { ok: true }) : redirect('ok');
|
||||
return wantsJson ? json(200, { ok: true }, cors) : redirect('ok');
|
||||
} catch (err) {
|
||||
// Defensive: unexpected throw (network error, bad construction).
|
||||
console.error('[waitlist] unexpected error', err);
|
||||
return wantsJson ? json(502, { ok: false, error: 'send_failed' }) : redirect('error');
|
||||
return wantsJson ? json(502, { ok: false, error: 'send_failed' }, cors) : redirect('error');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue