TheHideout-UI/src/App.jsx

410 lines
13 KiB
JavaScript

import PropTypes from "prop-types";
import { useState, useEffect, useRef } from "react";
import { Alert } from "antd";
import { useMediaQuery } from "react-responsive";
import { useNavigate, useLocation } from "react-router-dom";
import { Element, scroller } from "react-scroll";
import axios from "axios";
import Page from "./components/Page";
import Images from "./components/ImageSidebar";
import { ImageProvider, useImageContext } from "./contexts/ImageContext";
import LoadingModal from "./components/LoadingModal";
import { ActionProvider } from "./contexts/ActionContext";
import SubPage from "./components/SubPage";
import PropertyPage from "./components/PropertyPage";
import { MenuProvider } from "./contexts/MenuContext";
import { ThemeProvider } from "./contexts/ThemeContext";
import {
SettingsProvider,
useSettingsContext,
} from "./contexts/SettingsContext";
const apiUrl = import.meta.env.VITE_API_URL;
import "./global.css";
// Component that handles image loading after API data is fetched
const AppContent = ({ pages, properties, images }) => {
const { loadImages } = useImageContext();
const [loadedOnce, setLoadedOnce] = useState(false);
const [currentPage, setCurrentPage] = useState({ pageType: "landingPage" });
const [currentTheme, setCurrentTheme] = useState();
const [currentProperty, setCurrentProperty] = useState(null);
const [currentPageIdx, setCurrentPageIdx] = useState(0);
const [contentVisible, setContentVisible] = useState(true);
const [subPageVisible, setSubPageVisible] = useState(false);
const locationRef = useRef();
const previousPageRef = useRef(null);
const currentPageRef = useRef(currentPage);
const landingPages = pages.filter((page) => page.pageType == "landingPage");
const navigate = useNavigate();
const location = useLocation();
const settings = useSettingsContext();
const isMobile = useMediaQuery({ maxWidth: 800 });
// Load images when they become available
useEffect(() => {
if (!loadedOnce && images && images.length > 0) {
loadImages(images);
setLoadedOnce(true);
}
}, [images, loadImages, loadedOnce]);
const setPageTitle = (name) => {
document.title = name ? `${name} - The Hideout` : "The Hideout";
};
// Handle direct URL navigation
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
locationRef.current = location.pathname;
if (Object.keys(settings).length == 0) {
return;
}
// Check if the URL matches /properties/:slug
if (location.pathname.startsWith("/properties/")) {
const propertySlug = location.pathname.split("/properties/")[1];
if (propertySlug != "") {
const property = properties.find((p) => p.slug === propertySlug);
if (property) {
// Set previousPage when navigating to a property
if (currentPageRef.current.pageType !== "subPage") {
previousPageRef.current = { ...currentPageRef.current };
}
setCurrentProperty(property);
setContentVisible(true);
if (currentProperty != null || currentPage.pageType == "subPage") {
setSubPageVisible(false);
setTimeout(() => {
setSubPageVisible(true);
}, 500);
} else {
setSubPageVisible(true);
}
setPageTitle(property.name);
} else {
navigate(settings.redirects["404"]);
setCurrentProperty(null); // property not found
}
return; // exit early since we're on a property page
}
}
setCurrentProperty(null);
if (pages.length > 0 && location.pathname !== "/") {
const slug = location.pathname.slice(1); // Remove leading slash
const page = pages.find((p) => p.slug === slug);
if (page) {
// Set previousPage when navigating to a subpage
if (
page.pageType === "subPage" &&
currentPageRef.current.pageType !== "subPage"
) {
previousPageRef.current = { ...currentPageRef.current };
}
setCurrentPage(page);
setCurrentPageIdx(pages.indexOf(page));
setPageTitle(page.name);
if (page.pageType === "landingPage") {
// Ensure DOM has rendered before attempting to scroll
setSubPageVisible(false);
requestAnimationFrame(() => {
scroller.scrollTo(slug, {
duration: 0,
delay: 0,
smooth: "easeInOutQuart",
containerId: "app-container",
});
});
} else {
setContentVisible(false);
if (currentPage.pageType == page.pageType) {
setSubPageVisible(false);
setTimeout(() => {
setSubPageVisible(true);
}, 500);
} else {
setSubPageVisible(true);
}
setTimeout(() => {
setContentVisible(true);
}, 600);
}
} else {
navigate(settings.redirects["404"]);
}
} else if (
pages.length > 0 &&
location.pathname === "/" &&
settings?.redirects?.index != null
) {
console.log("settings.redirects", settings.redirects);
const indexPage = pages.find((p) => p.slug === settings.redirects.index);
// Default to index page
setCurrentPage(indexPage);
setCurrentPageIdx(0);
setPageTitle(indexPage.name);
navigate(`/${indexPage.slug}`, { replace: true });
}
}, [
location.pathname,
pages,
settings?.redirects?.index,
navigate,
settings,
properties,
currentPage.pageType,
currentProperty,
]);
// Set up scroll spy to update URL when scrolling
useEffect(() => {
if (currentPage.pageType == "subPage") {
return;
}
if (pages.length > 0) {
let scrollTimeout;
const handleScrollSpy = () => {
const container = document.getElementById("app-container");
if (!container) return;
const scrollTop = container.scrollTop;
const containerHeight = container.clientHeight;
const viewportCenter = scrollTop + containerHeight / 2;
// Find which page is currently in the center of the viewport
let currentPageIndex = 0;
let minDistance = Infinity;
pages.forEach((page, index) => {
const element = document.querySelector(`[data-name="${page.slug}"]`);
if (element) {
const elementTop = element.offsetTop;
//const elementBottom = elementTop + element.offsetHeight;
const elementCenter = elementTop + element.offsetHeight / 2;
const distance = Math.abs(viewportCenter - elementCenter);
if (distance < minDistance) {
minDistance = distance;
currentPageIndex = index;
if (minDistance != 0) {
setContentVisible(false);
}
if (minDistance == 0) {
setContentVisible(true);
}
}
}
});
// Debounce both current page state and URL updates to reduce frequency
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
if (pages[currentPageIndex] && pages[currentPageIndex].slug) {
// Update current page state for Images component
if (currentPageIdx !== currentPageIndex) {
setCurrentPage(pages[currentPageIndex]);
setCurrentPageIdx(currentPageIndex);
setPageTitle(pages[currentPageIndex].name);
const newPath = `/${pages[currentPageIndex].slug}`;
if (locationRef.current !== newPath) {
navigate(newPath, { replace: true });
}
}
}
}, 100); // Debounce for 100ms
};
const container = document.getElementById("app-container");
if (container) {
container.addEventListener("scroll", handleScrollSpy, {
passive: true,
});
return () => {
container.removeEventListener("scroll", handleScrollSpy);
clearTimeout(scrollTimeout);
};
}
}
}, [pages, navigate, currentPageIdx, currentPage.pageType]);
// Update currentPageRef whenever currentPage changes
useEffect(() => {
currentPageRef.current = currentPage;
}, [currentPage]);
// Set body background color to match current page's theme
useEffect(() => {
const theme = settings.themes.find(
(theme) => theme.name === currentPage?.theme
);
setCurrentTheme(theme);
}, [currentPage, settings.themes]);
// Handle subpage close with smart navigation
const handleSubPageClose = () => {
// Check if there's a previous page stored in ref
if (previousPageRef.current && previousPageRef.current.slug) {
// Navigate to the previous page's slug
navigate(`/${previousPageRef.current.slug}`);
} else {
// No previous page, redirect to home
navigate("/");
}
};
return (
<ThemeProvider currentTheme={currentTheme}>
<MenuProvider pages={pages} currentPageSlug={currentPage?.slug}>
<div
className={
!isMobile
? "th-app-container"
: "th-app-container th-app-container-mobile"
}
id="app-container"
>
{landingPages.map((pageData, index) => (
<Element
key={index}
name={pageData.slug}
className={
!isMobile
? "th-page-wrapper"
: "th-page-wrapper th-page-wrapper-mobile"
}
data-name={pageData.slug}
>
<Page
pageData={pageData}
visible={
contentVisible && pageData.notionId == currentPage.notionId
}
theme={settings.themes.find(
(theme) => theme.name === pageData.theme
)}
themes={settings.themes}
isSubPage={false}
id={index}
/>
</Element>
))}
</div>
<SubPage
visible={currentPage.pageType == "subPage" && subPageVisible == true}
>
<Page
pageData={currentPage}
visible={contentVisible}
theme={settings.themes.find(
(theme) => theme.name === currentPage.theme
)}
themes={settings.themes}
isSubPage={true}
id={currentPageIdx}
showClose={true}
showMenu={false}
onClose={handleSubPageClose}
/>
</SubPage>
<SubPage visible={currentProperty != null && subPageVisible == true}>
<PropertyPage
propertyData={currentProperty}
visible={contentVisible}
onClose={() => {
navigate("/" + settings.redirects.properties);
}}
/>
</SubPage>
{isMobile && <div className="th-mobile-overlay" />}
{!isMobile && (
<Images
imageCollections={[...pages, ...properties]}
currentSlug={currentProperty?.slug || currentPage?.slug}
/>
)}
</MenuProvider>
</ThemeProvider>
);
};
AppContent.propTypes = {
images: PropTypes.shape({
length: PropTypes.number,
}),
pages: PropTypes.array.isRequired,
properties: PropTypes.array.isRequired,
themes: PropTypes.shape({
find: PropTypes.func,
}),
};
const App = () => {
const [pages, setPages] = useState([]);
const [properties, setProperties] = useState([]);
const [images, setImages] = useState([]);
const [settings, setSettings] = useState({
themes: [],
redirects: {},
branding: [],
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const actions = {};
useEffect(() => {
const fetchPages = async () => {
try {
setLoading(true);
const response = await axios.get(`${apiUrl}/content`);
setPages(response.data.pages || response.data); // Handle both array and object with pages property
setProperties(response.data.properties || response.data);
setImages(response.data.images || response.data);
setSettings(response.data.settings || response.data);
} catch (err) {
setError(
err.response?.data?.message || err.message || "Failed to fetch pages"
);
console.error("Error fetching pages:", err);
} finally {
setLoading(false);
}
};
fetchPages();
}, []);
if (error) {
return (
<div className="th-error-container">
<Alert
message="Error Loading Pages"
description={error}
type="error"
showIcon
/>
</div>
);
}
return (
<SettingsProvider settings={settings}>
<ActionProvider onAction={actions}>
<ImageProvider>
<AppContent pages={pages} images={images} properties={properties} />
<LoadingModal visible={loading} />
</ImageProvider>
</ActionProvider>
</SettingsProvider>
);
};
export default App;