feat: subsites
This commit is contained in:
@@ -1,6 +1,36 @@
|
||||
/* player.css moved to /public/styles/player.css — kept empty here to avoid
|
||||
accidental inclusion by the build tool during development. */
|
||||
/* Universal box-sizing for consistency */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--lrc-text-color: #333; /* darker text */
|
||||
--lrc-bg-color: rgba(0, 0, 0, 0.05);
|
||||
--lrc-active-color: #000; /* bold black for active */
|
||||
--lrc-active-shadow: none; /* no glow in light mode */
|
||||
--lrc-hover-color: #005fcc; /* darker blue hover */
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--lrc-text-color: #ccc; /* original gray */
|
||||
--lrc-bg-color: rgba(255, 255, 255, 0.02);
|
||||
--lrc-active-color: #fff; /* bright white for active */
|
||||
--lrc-active-shadow: 0 0 4px rgba(212, 175, 55, 0.6); /* gold glow */
|
||||
--lrc-hover-color: #4fa2ff; /* original blue hover */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 100vw;
|
||||
min-height: 100vh;
|
||||
/* background: linear-gradient(-45deg, #FFCDD2 50%, #B2EBF2 50%); */
|
||||
}
|
||||
|
||||
/* Container for the player and album cover */
|
||||
.music-container {
|
||||
|
||||
@@ -30,11 +30,10 @@ export default function Root({ child, user = undefined, ...props }) {
|
||||
// runtime flag `window.__IS_SUBSITE` from the server layout so pages
|
||||
// don't need to pass guards.
|
||||
const isSubsite = typeof document !== 'undefined' && document.documentElement.getAttribute('data-subsite') === 'true';
|
||||
// Helpful runtime debugging: only log when child changes so we don't spam
|
||||
// the console on every render. Use an effect so output is stable.
|
||||
// Log when the active child changes (DEV only)
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof console !== 'undefined' && typeof document !== 'undefined') {
|
||||
if (typeof console !== 'undefined' && typeof document !== 'undefined' && import.meta.env.DEV) {
|
||||
console.debug(`[AppLayout] child=${String(child)}, data-subsite=${document.documentElement.getAttribute('data-subsite')}`);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -58,13 +57,13 @@ export default function Root({ child, user = undefined, ...props }) {
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
if (wantPlayer) {
|
||||
try { console.debug('[AppLayout] dynamic-import: requesting AudioPlayer'); } catch (e) { }
|
||||
if (import.meta.env.DEV) { try { console.debug('[AppLayout] dynamic-import: requesting AudioPlayer'); } catch (e) { } }
|
||||
import('./AudioPlayer.jsx')
|
||||
.then((mod) => {
|
||||
if (!mounted) return;
|
||||
// set the component factory
|
||||
setPlayerComp(() => mod.default ?? null);
|
||||
try { console.debug('[AppLayout] AudioPlayer import succeeded'); } catch (e) { }
|
||||
if (import.meta.env.DEV) { try { console.debug('[AppLayout] AudioPlayer import succeeded'); } catch (e) { } }
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('[AppLayout] AudioPlayer import failed', err);
|
||||
@@ -94,7 +93,7 @@ export default function Root({ child, user = undefined, ...props }) {
|
||||
{child == "LoginPage" && (<LoginPage {...props} loggedIn={loggedIn} />)}
|
||||
{child == "LyricSearch" && (<LyricSearch {...props} client:only="react" />)}
|
||||
{child == "Player" && !isSubsite && PlayerComp && (
|
||||
<Suspense fallback={<div data-testid="player-fallback" className="p-4 text-center">Loading player...</div>}>
|
||||
<Suspense fallback={null}>
|
||||
<PlayerComp client:only="react" user={user} />
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, { useState, useEffect, useRef, Suspense, lazy, useMemo, useCallback } from "react";
|
||||
import "@styles/player.css";
|
||||
import { metaData } from "../config";
|
||||
import Play from "@mui/icons-material/PlayArrow";
|
||||
import Pause from "@mui/icons-material/Pause";
|
||||
// Load AudioPlayer CSS at runtime only when the player mounts on the client.
|
||||
// This avoids including the stylesheet in pages where the AudioPlayer never
|
||||
// gets loaded (subsidiary/whitelabel sites). We dynamically import the
|
||||
// stylesheet inside a client-only effect below.
|
||||
import "@styles/player.css";
|
||||
import { Dialog } from "primereact/dialog";
|
||||
import { AutoComplete } from "primereact/autocomplete";
|
||||
import { DataTable } from "primereact/datatable";
|
||||
@@ -51,31 +49,6 @@ const STATIONS = {
|
||||
|
||||
|
||||
export default function Player({ user }) {
|
||||
// Log lifecycle so we can confirm the player mounted in the browser
|
||||
useEffect(() => {
|
||||
// Load the player stylesheet from /public/styles/player.css so it will be
|
||||
// requested at runtime only when the AudioPlayer mounts. This prevents the
|
||||
// CSS from being included as a route asset for subsites.
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (!document.getElementById('audio-player-css')) {
|
||||
const link = document.createElement('link');
|
||||
link.id = 'audio-player-css';
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/styles/player.css';
|
||||
link.onload = () => { try { console.debug('[AudioPlayer] CSS loaded (link)'); } catch (e) { } };
|
||||
link.onerror = (err) => { console.warn('[AudioPlayer] CSS link failed', err); };
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
try { console.debug('[AudioPlayer] mounted'); } catch (e) { }
|
||||
return () => {
|
||||
try { console.debug('[AudioPlayer] unmounted'); } catch (e) { }
|
||||
};
|
||||
}, []);
|
||||
// Inject custom paginator styles
|
||||
useEffect(() => {
|
||||
const styleId = 'queue-paginator-styles';
|
||||
|
||||
@@ -20,7 +20,7 @@ const trimmedTitle = title?.trim();
|
||||
const seoTitle = trimmedTitle || metaData.title;
|
||||
const shareTitle = isWhitelabel ? (trimmedTitle || (metaData.shareTitle ?? metaData.title)) : (trimmedTitle ? `${trimmedTitle} | ${metaData.title}` : (metaData.shareTitle ?? metaData.title));
|
||||
const seoTitleTemplate = isWhitelabel ? "%s" : (trimmedTitle ? `%s | ${metaData.title}` : "%s");
|
||||
const shareDescription = description ?? metaData.shareDescription ?? metaData.description;
|
||||
const shareDescription = isWhitelabel ? "%s" : (description ?? metaData.shareDescription ?? metaData.description);
|
||||
const canonicalUrl = url?.href ?? metaData.baseUrl;
|
||||
const shareImage = new URL(image ?? metaData.ogImage, metaData.baseUrl).toString();
|
||||
const shareImageAlt = metaData.shareImageAlt ?? metaData.shareTitle ?? metaData.title;
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Lighting() {
|
||||
}, []);
|
||||
|
||||
const handleColorChange = (color) => {
|
||||
console.log('Handle color change:', color);
|
||||
if (import.meta.env.DEV) console.debug('Handle color change:', color);
|
||||
const { r, g, b } = color.rgb;
|
||||
updateLighting({
|
||||
...state,
|
||||
@@ -128,7 +128,7 @@ export default function Lighting() {
|
||||
s / 100, // saturation: 0-100 -> 0-1
|
||||
(v ?? 100) / 100 // value: 0-100 -> 0-1, default to 1 if undefined
|
||||
);
|
||||
console.log('Converting color:', color.hsva, 'to RGB:', rgb);
|
||||
if (import.meta.env.DEV) console.debug('Converting color:', color.hsva, 'to RGB:', rgb);
|
||||
updateLighting({
|
||||
...state,
|
||||
red: rgb.red,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import { Button } from "@mui/joy";
|
||||
import { Dropdown } from "primereact/dropdown/dropdown.esm.js";
|
||||
// Dropdown not used in this form; removed to avoid unused-import warnings
|
||||
import { AutoComplete } from "primereact/autocomplete/autocomplete.esm.js";
|
||||
import { InputText } from "primereact/inputtext/inputtext.esm.js";
|
||||
|
||||
@@ -133,7 +133,8 @@ export default function ReqForm() {
|
||||
if (e.value.year) setYear(e.value.year);
|
||||
setSelectedOverview(e.value.overview || "");
|
||||
}}
|
||||
placeholder="Enter movie or TV show title"
|
||||
placeholder="Enter movie or TV title"
|
||||
title="Enter movie or TV show title"
|
||||
className="w-full"
|
||||
inputClassName="w-full border-2 border-gray-200 dark:border-gray-600 rounded-xl px-4 py-3 focus:border-[#12f8f4] transition-colors"
|
||||
panelClassName="border-2 border-gray-200 dark:border-gray-600 rounded-xl"
|
||||
|
||||
@@ -32,8 +32,6 @@ export const WHITELABELS = {
|
||||
};
|
||||
|
||||
// Subsite mapping: host -> site path
|
||||
// Keep this in sync with your page layout (e.g. src/pages/subsites/req/ -> /subsites/req)
|
||||
export const SUBSITES = {
|
||||
'req.boatson.boats': '/subsites/req',
|
||||
'subsite2.codey.lol': '/subsite2',
|
||||
};
|
||||
@@ -56,7 +56,9 @@ if (!whitelabel) {
|
||||
const isSubsite = (Astro.request as any)?.locals?.isSubsite ?? Boolean(whitelabel || detectedSubsite);
|
||||
|
||||
// Debug logging
|
||||
console.log(`[Base.astro] host: ${host}, forcedParam: ${forcedParam}, isReq: ${isReq}, whitelabel: ${JSON.stringify(whitelabel)}`);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`[Base.astro] host: ${host}, forcedParam: ${forcedParam}, isReq: ${isReq}, whitelabel: ${JSON.stringify(whitelabel)}`);
|
||||
}
|
||||
---
|
||||
|
||||
<html lang="en" class="scrollbar-hide lenis lenis-smooth" data-subsite={isSubsite ? 'true' : 'false'}>
|
||||
@@ -68,7 +70,6 @@ console.log(`[Base.astro] host: ${host}, forcedParam: ${forcedParam}, isReq: ${i
|
||||
/>
|
||||
<Themes />
|
||||
<BaseHead title={whitelabel?.siteTitle ?? title} description={description} image={image ?? metaData.ogImage} isWhitelabel={!!whitelabel} />
|
||||
<!-- Subsite state is available on the html[data-subsite] attribute -->
|
||||
<script>
|
||||
import "@scripts/lenisSmoothScroll.js";
|
||||
import "@scripts/main.jsx";
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
// Default subsite nav — used when a subsite exists but no specialized nav is available
|
||||
import { API_URL } from "../../config";
|
||||
const whitelabel = Astro.props?.whitelabel ?? null;
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<script src="/scripts/nav-controls.js" defer data-api-url={API_URL}></script>
|
||||
|
||||
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<a href="/" class="text-xl sm:text-2xl font-semibold" style={`color: ${whitelabel?.brandColor ?? 'var(--brand-color)'}`}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
// Req specific subsite nav placeholder. Keeps markup minimal for now.
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { API_URL } from "../../config";
|
||||
const whitelabel = Astro.props?.whitelabel ?? null;
|
||||
const currentPath = Astro.url.pathname;
|
||||
|
||||
@@ -9,13 +10,14 @@ const links = [
|
||||
];
|
||||
---
|
||||
|
||||
<script src="/scripts/nav-controls.js" defer data-api-url={API_URL}></script>
|
||||
|
||||
<nav class="w-full px-4 sm:px-6 py-4 bg-transparent sticky top-0 z-50 backdrop-blur-sm bg-white/80 dark:bg-[#121212]/80 border-b border-neutral-200/50 dark:border-neutral-800/50">
|
||||
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
||||
<a href="/" class="text-xl sm:text-2xl font-semibold" style={`color: ${whitelabel?.brandColor ?? 'var(--brand-color)'}`}>
|
||||
{whitelabel?.logoText ?? 'REQ'}
|
||||
</a>
|
||||
<ul class="flex items-center gap-4">
|
||||
<!-- currently empty; future subsite-specific links go here -->
|
||||
<li>
|
||||
<button aria-label="Toggle theme" type="button" class="flex items-center justify-center w-8 h-8 rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-800 transition-colors" onclick="toggleTheme()">
|
||||
<Icon name="fa6-solid:circle-half-stroke" class="h-4 w-4 text-[#1c1c1c] dark:text-[#D4D4D4]" />
|
||||
|
||||
@@ -18,14 +18,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const host = (hostHeader || authorityHeader || urlHost).split(':')[0]; // normalize remove port
|
||||
|
||||
// Debug info for incoming requests
|
||||
console.log(`[middleware] incoming: host=${hostHeader} path=${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] incoming: host=${hostHeader} path=${context.url.pathname}`);
|
||||
|
||||
// When the host header is missing, log all headers for debugging and
|
||||
// attempt to determine host from forwarded headers or a dev query header.
|
||||
if (!host) {
|
||||
console.log('[middleware] WARNING: Host header missing. Dumping headers:');
|
||||
if (import.meta.env.DEV) console.log('[middleware] WARNING: Host header missing. Dumping headers:');
|
||||
for (const [k, v] of context.request.headers) {
|
||||
console.log(`[middleware] header: ${k}=${v}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] header: ${k}=${v}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const shouldSkip = skipPrefixes.some((p) => context.url.pathname.startsWith(p));
|
||||
|
||||
if (!shouldSkip && !context.url.pathname.startsWith(subsitePath)) {
|
||||
console.log(`[middleware] Rewriting ${host} ${context.url.pathname} -> ${subsitePath}${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Rewriting ${host} ${context.url.pathname} -> ${subsitePath}${context.url.pathname}`);
|
||||
context.url.pathname = `${subsitePath}${context.url.pathname}`;
|
||||
}
|
||||
} else {
|
||||
@@ -67,7 +67,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const allPaths = Object.values(subsites || {});
|
||||
const pathLooksLikeSubsite = allPaths.some((p) => context.url.pathname.startsWith(p));
|
||||
if (pathLooksLikeSubsite) {
|
||||
console.log(`[middleware] Blocking subsite path on main domain: ${host}${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite path on main domain: ${host}${context.url.pathname}`);
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
}
|
||||
@@ -77,10 +77,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
if (isMainDomain && Object.values(subsites || {}).some(p => context.url.pathname.startsWith(p))) {
|
||||
// Immediately return a 404 for /req on the main domain
|
||||
if (Object.values(subsites || {}).includes(context.url.pathname)) {
|
||||
console.log(`[middleware] Blocking subsite root on main domain: ${hostHeader}${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite root on main domain: ${hostHeader}${context.url.pathname}`);
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
console.log(`[middleware] Blocking subsite wildcard path on main domain: ${hostHeader}${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Blocking subsite wildcard path on main domain: ${hostHeader}${context.url.pathname}`);
|
||||
return new Response('Not found', { status: 404 });
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
if (forceWhitelabelQuery) {
|
||||
const forced = getSubsiteFromSignal(forceWhitelabelQuery);
|
||||
const subsitePath = forced?.path || (`/${forceWhitelabelQuery}`.startsWith('/') ? `/${forceWhitelabelQuery}` : `/${forceWhitelabelQuery}`);
|
||||
console.log(`[middleware] Forcing whitelabel via query: ${forceWhitelabelQuery} -> rewrite to ${subsitePath}${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Forcing whitelabel via query: ${forceWhitelabelQuery} -> rewrite to ${subsitePath}${context.url.pathname}`);
|
||||
context.url.pathname = `${subsitePath}${context.url.pathname}`;
|
||||
}
|
||||
|
||||
@@ -101,17 +101,17 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
||||
// Expose a simple boolean so server-rendered pages/layouts or components can opt-out of loading/hydrating
|
||||
// heavyweight subsystems (like the AudioPlayer) when the request is for a subsite.
|
||||
context.locals.isSubsite = Boolean(wantsSubsite);
|
||||
console.log(`[middleware] Setting context.locals.whitelabel: ${context.locals.whitelabel}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Setting context.locals.whitelabel: ${context.locals.whitelabel}`);
|
||||
|
||||
// Final debug: show the final pathname we hand to Astro
|
||||
console.log(`[middleware] Final pathname: ${context.url.pathname}`);
|
||||
if (import.meta.env.DEV) console.log(`[middleware] Final pathname: ${context.url.pathname}`);
|
||||
|
||||
// Let Astro handle the request first
|
||||
const response = await next();
|
||||
|
||||
// If it's a 404, redirect to home
|
||||
if (response.status === 404 && !context.url.pathname.startsWith('/api/')) {
|
||||
console.log(`404 redirect: ${context.url.pathname} -> /`);
|
||||
if (import.meta.env.DEV) console.log(`404 redirect: ${context.url.pathname} -> /`);
|
||||
return context.redirect('/', 302);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user