React behind /react basePath; nginx restores 80 to PHP; cross-verify suite

Co-existence on 201: 80=PHP gnuboard5 (default vhost), /react=Next.js (basePath)
- Dockerfile takes ARG NEXT_PUBLIC_BASE_PATH, passes to Next build for asset/route prefixing
- next.config.mjs reads NEXT_PUBLIC_BASE_PATH at build time
- nginx slot-clone vhost: location /react proxies to 3000 unchanged; / and /adm/ go to 8090
- /robots.txt remains nginx-served (all-bot block)

Design polish:
- Header MegaPanel sub-link: gradient hover + white text + shadow on hover (was hover:bg-brand-50 only)
- Top-level mega menu: bigger padding + bold + bottom-bar hover indicator
- StatStrip: 8 narrow cards → 4 large cards (sm:grid-cols-4) with 12 size icon, 28px value, blur halo

Verify:
- scripts/verify-cross.mjs: parallel PHP (admin/clone1234) + React (testlogin/test1234) flow
- 50 iterations × 11 checks = 550/550 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
chpark
2026-04-28 14:17:43 +09:00
parent df72d3888a
commit 4e6a304615
5 changed files with 148 additions and 10 deletions
+2
View File
@@ -1,6 +1,8 @@
/** @type {import('next').NextConfig} */
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || '';
const nextConfig = {
reactStrictMode: true,
...(basePath ? { basePath, assetPrefix: basePath } : {}),
transpilePackages: ['@slot/themes', '@slot/db', '@slot/auth'],
serverExternalPackages: ['postgres', '@node-rs/argon2'],
typescript: { ignoreBuildErrors: true },
@@ -166,7 +166,7 @@ function MegaPanel({ items }: { items: MenuItem[] }) {
<li key={ci}>
<Link
href={c.href || '#'}
className="block rounded-lg px-2.5 py-1.5 text-[13px] text-neutral-700 hover:bg-brand-50 hover:text-brand-700 whitespace-nowrap"
className="block rounded-lg px-3 py-2 text-[13.5px] font-medium text-neutral-800 transition hover:bg-gradient-to-r hover:from-brand-600 hover:to-fuchsia-600 hover:text-white hover:shadow-[0_4px_14px_rgba(124,82,224,0.25)] whitespace-nowrap"
>
{c.label}
</Link>
@@ -195,7 +195,7 @@ function MegaNav({ menus, isDark: _isDark }: { menus: MenuItem[]; isDark: boolea
>
<Link
href={m.href || '#'}
className="flex items-center gap-1.5 whitespace-nowrap px-3 py-3.5 text-[14px] font-semibold tracking-tight hover:bg-white/10"
className="flex items-center gap-1.5 whitespace-nowrap px-4 py-3.5 text-[14.5px] font-bold tracking-tight text-white transition hover:bg-white/20 hover:shadow-[inset_0_-3px_0_0_rgba(255,255,255,0.85)]"
>
{m.icon && <span aria-hidden>{m.icon}</span>}
<span>{m.label}</span>
@@ -32,7 +32,7 @@ export default function StatStrip({ members, posts, comments, visitsToday, visit
];
return (
<section className="grid grid-cols-2 gap-2.5 sm:grid-cols-4 lg:grid-cols-8">
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{stats.map((s, i) => {
const Icon = s.icon;
return (
@@ -41,17 +41,17 @@ export default function StatStrip({ members, posts, comments, visitsToday, visit
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.04, duration: 0.4 }}
className="lift relative overflow-hidden rounded-2xl bg-white p-3.5 ring-1 ring-neutral-100"
className="lift relative overflow-hidden rounded-3xl bg-white p-5 ring-1 ring-neutral-100 shadow-[0_8px_24px_rgba(124,82,224,0.06)]"
>
<div className={`absolute -top-6 -right-6 h-20 w-20 rounded-full bg-gradient-to-br ${s.tone} opacity-20 blur-2xl`} />
<div className={`absolute -top-8 -right-8 h-32 w-32 rounded-full bg-gradient-to-br ${s.tone} opacity-25 blur-3xl`} />
<div className="flex items-start justify-between">
<span className={`grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br ${s.tone} text-white shadow-[0_6px_14px_rgba(0,0,0,0.10)]`}>
<Icon size={16} />
<span className={`grid h-12 w-12 place-items-center rounded-2xl bg-gradient-to-br ${s.tone} text-white shadow-[0_8px_22px_rgba(0,0,0,0.18)]`}>
<Icon size={20} />
</span>
<span className="text-[10px] font-semibold uppercase tracking-wider text-neutral-text-soft">{s.sub}</span>
<span className="rounded-full bg-neutral-50 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-neutral-600">{s.sub}</span>
</div>
<div className="mt-2 text-[20px] font-extrabold tabular text-neutral-900">{s.value}</div>
<div className="text-[11px] font-medium text-neutral-text-soft">{s.label}</div>
<div className="mt-3 text-[28px] font-extrabold tabular text-neutral-900">{s.value}</div>
<div className="text-[12px] font-semibold text-neutral-text-soft">{s.label}</div>
</motion.div>
);
})}