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; // 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 (
{landingPages.map((pageData, index) => ( theme.name === pageData.theme )} themes={settings.themes} isSubPage={false} id={index} /> ))}
theme.name === currentPage.theme )} themes={settings.themes} isSubPage={true} id={currentPageIdx} showClose={true} showMenu={false} onClose={handleSubPageClose} /> { navigate("/" + settings.redirects.properties); }} /> {isMobile &&
} {!isMobile && ( )} ); }; 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 (
); } return ( ); }; export default App;