mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
Migrate logicsrc-web from Vite SPA to Next.js 16 App Router
Replaces the Vite single-page app + custom Node server.js with a Next.js 16.2.6 App Router app. - proxy.ts (src/proxy.ts): www.logicsrc.com -> logicsrc.com 301 over https, preserving path + query (the original request, now via Next 16 Proxy). - One SSR page via an optional catch-all ([[...slug]]) that renders the same marketing/spec page for each known top-level route (/docs, /blog, /openspec, ...) and 404s unknown paths, preserving existing canonical URLs. Markup is a faithful server-rendered port of the old main.ts (SEO upgrade over the prior client render); interactivity (hire-us form, CoinPay button, section scroll) moves to a client component. - API routes ported to app/api/**: hire-us coinpay-checkout + project-request, oauth/coinpay start/callback/session, webhooks/coinpay. Shared logic in src/lib/coinpay.ts (eligibility, payment-rail selection, webhook verify, HMAC session sign/verify, cookies). - commandboard-api (/health + /api/boards|tasks|plugins/*) is no longer mounted in-process; next.config.ts proxies those paths to COMMANDBOARD_API_URL via afterFiles rewrites (our own /api routes match first). - Build/start switch to next build / next start. Contract tests rewritten to exercise proxy.ts, the route handlers, and pure helpers directly (21 passing); Playwright webServer updated. Deployment (Railway): set COMMANDBOARD_API_URL to the commandboard-api service URL and run it as its own service; root start now runs next start. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
cf5d526176
commit
f0a9f3890e
24 changed files with 2102 additions and 1394 deletions
120
apps/logicsrc-web/src/components/home-interactivity.tsx
Normal file
120
apps/logicsrc-web/src/components/home-interactivity.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
// Client-side behavior for the LogicSRC page, ported from the legacy Vite
|
||||
// main.ts: register the service worker, wire the hire-us project form, reflect
|
||||
// CoinPay connection status on the Connect button, and scroll to the section
|
||||
// that matches the current path. The markup these hooks target is rendered on
|
||||
// the server by `renderPageMarkup`.
|
||||
export function HomeInteractivity(): null {
|
||||
useEffect(() => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/service-worker.js").catch(() => undefined);
|
||||
}
|
||||
|
||||
const buildParagraph = (text: string): HTMLParagraphElement => {
|
||||
const paragraph = document.createElement("p");
|
||||
paragraph.textContent = text;
|
||||
return paragraph;
|
||||
};
|
||||
|
||||
const form = document.querySelector<HTMLFormElement>("#project-request-form");
|
||||
const onSubmit = async (event: Event): Promise<void> => {
|
||||
event.preventDefault();
|
||||
const button = document.querySelector<HTMLButtonElement>("#project-request-button");
|
||||
const result = document.querySelector<HTMLDivElement>("#project-request-result");
|
||||
const contact = document.querySelector<HTMLInputElement>("#project-contact");
|
||||
const project = document.querySelector<HTMLTextAreaElement>("#project-description");
|
||||
if (!button || !result || !contact || !project) return;
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = "Submitting...";
|
||||
result.replaceChildren(buildParagraph("Submitting project request."));
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/hire-us/project-request", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ contact: contact.value, project: project.value })
|
||||
});
|
||||
const payload = await response.json();
|
||||
|
||||
if (!response.ok || !payload.success) {
|
||||
throw new Error(payload.error || "Project request could not be submitted.");
|
||||
}
|
||||
|
||||
result.replaceChildren(
|
||||
buildParagraph("Request received. If it is a fit, we will send a $250/week recurring CoinPay invoice.")
|
||||
);
|
||||
} catch (error) {
|
||||
result.replaceChildren(
|
||||
buildParagraph(error instanceof Error ? error.message : "Project request could not be submitted.")
|
||||
);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = "Request review";
|
||||
}
|
||||
};
|
||||
form?.addEventListener("submit", onSubmit);
|
||||
|
||||
// Scroll to the section that matches the current path (mirrors the SPA).
|
||||
const { pathname } = window.location;
|
||||
if (pathname === "/agent-swarm") {
|
||||
document.querySelector("#agent-swarm")?.scrollIntoView();
|
||||
} else if (pathname === "/agentbyte") {
|
||||
document.querySelector("#agentbyte")?.scrollIntoView();
|
||||
} else {
|
||||
const pageRoute = pathname.slice(1);
|
||||
if (
|
||||
["docs", "blog", "openspec", "credential-sharing", "hire-us", "about", "terms", "privacy"].includes(pageRoute)
|
||||
) {
|
||||
document.querySelector(`#${pageRoute}`)?.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
// CoinPay OAuth connection status: clean the query param and reflect state.
|
||||
const coinpayParam = new URLSearchParams(window.location.search).get("coinpay_oauth");
|
||||
if (coinpayParam) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("coinpay_oauth");
|
||||
url.searchParams.delete("error");
|
||||
history.replaceState(null, "", url.pathname + (url.search || ""));
|
||||
}
|
||||
|
||||
const updateCoinPayButton = async (): Promise<void> => {
|
||||
const connectBtn = document.querySelector<HTMLAnchorElement>(
|
||||
".hero-actions a[href='/api/oauth/coinpay/start']"
|
||||
);
|
||||
if (!connectBtn) return;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/oauth/coinpay/session");
|
||||
const data = await res.json();
|
||||
|
||||
if (data.authenticated && data.user) {
|
||||
const label = data.user.email || data.user.name || data.user.sub || "CoinPay";
|
||||
connectBtn.textContent = `Connected: ${label}`;
|
||||
connectBtn.style.background = "#3a9e7e";
|
||||
connectBtn.removeAttribute("href");
|
||||
connectBtn.style.cursor = "default";
|
||||
connectBtn.title = `Connected via CoinPay since ${new Date(data.user.connected_at).toLocaleDateString()}`;
|
||||
} else if (coinpayParam === "connected") {
|
||||
connectBtn.textContent = "CoinPay Connected";
|
||||
connectBtn.style.background = "#3a9e7e";
|
||||
} else if (coinpayParam === "error") {
|
||||
connectBtn.textContent = "Connect CoinPay (retry)";
|
||||
}
|
||||
} catch {
|
||||
// session check failed — leave button as-is
|
||||
}
|
||||
};
|
||||
void updateCoinPayButton();
|
||||
|
||||
return () => {
|
||||
form?.removeEventListener("submit", onSubmit);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue