Skip to content

Commit

Permalink
Merge pull request #1430 from massenergize/introducing-custom-pages
Browse files Browse the repository at this point in the history
Introducing custom pages
  • Loading branch information
frimpongopoku authored Nov 28, 2024
2 parents 112ba13 + b826b56 commit 9303b8d
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 4 deletions.
11 changes: 8 additions & 3 deletions src/AppRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import METoast from "./components/Pages/Widgets/METoast/METoast";
import { child } from "firebase/database";
import RewiringAmerica from "./components/Pages/RewiringAmerica.js";
import OneTestimonialV2 from "./components/Pages/StoriesPage/OneTestimonialV2.js";
import RenderCustomPage from "./components/Pages/Custom Pages/RenderCustomPage.js";

class AppRouter extends Component {
constructor(props) {
Expand Down Expand Up @@ -417,7 +418,7 @@ class AppRouter extends Component {
if (children && children.length > 0) {
children = children.map((child) => this.prependPrefix(child, prefix));
}
if(!rest?.is_link_external && !link?.startsWith("/")){
if (!rest?.is_link_external && !link?.startsWith("/")) {
link = `/${link}`;
}
return {
Expand Down Expand Up @@ -455,12 +456,11 @@ class AppRouter extends Component {
link: URLS.COMMUNITIES, //"http://" + window.location.host,
special: true,
};
footerLinks = footerLinks.map(m => this.prependPrefix(m, prefix));
footerLinks = footerLinks.map((m) => this.prependPrefix(m, prefix));
footerLinks.push(communitiesLink);
this.setState({ footerLinks: footerLinks });
}


/**
* Adds the prefix to the subdomains where possible
* @param {*} menu
Expand Down Expand Up @@ -597,6 +597,11 @@ class AppRouter extends Component {
<Switch>
{/* ---- This route is a facebook app requirement. -------- */}
<Route path={`/how-to-delete-my-data`} component={Help} />
<Route
exact
path={`/${community?.subdomain}/c/:pageId`}
component={RenderCustomPage}
/>
<Route
exact
path={`${links.profile}/password-less/manage`}
Expand Down
51 changes: 51 additions & 0 deletions src/components/Pages/Custom Pages/RenderCustomPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useEffect } from "react";
import { useParams } from "react-router-dom/cjs/react-router-dom.min";
import { useApiRequest } from "../../../hooks/useApiRequest";
import LoadingCircle from "../../Shared/LoadingCircle";
import PBPublishedRender from "./render/PBPublishedRender";

function RenderCustomPage() {
const { pageId } = useParams();

const [pageLoadHandler] = useApiRequest([
{
key: "pageLoad",
url: "/custom.pages.getForUser",
},
]);

const [fetchPageInfo, data, error, loading] = pageLoadHandler || [];

useEffect(() => {
fetchPageInfo({ id: pageId });
}, [pageId]);

if (loading) return <LoadingCircle />;

if (error)
return (
<div style={{ height: "100vh" }}>
<p
style={{
width: "100%",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
padding: "20px",
color: "#d05c5c",
}}
>
{error}
</p>
</div>
);

return (
<div style={{ height: "100vh" }}>
<PBPublishedRender sections={data?.content || []} />
</div>
);
}

export default RenderCustomPage;
51 changes: 51 additions & 0 deletions src/components/Pages/Custom Pages/render/PBPublishedRender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { useEffect, useMemo, useRef } from "react";
import { renderSection, serializeBlock } from "./engine/engine";

function PBPublishedRender({ sections }) {
const iframeRef = useRef();
const contRef = useRef();

const html = useMemo(
() =>
sections
.map(({ block }) => {
return serializeBlock(block?.template);
})
?.join(""),
[sections]
);

useEffect(() => {
if (iframeRef?.current) {
const doc = iframeRef.current?.contentDocument || iframeRef.current?.contentWindow?.document;
doc.open();
doc.write(`
<html>
<head>
<style>
@import url("https://fonts.googleapis.com/css?family=Google+Sans:400,400i,500,500i,600,600i,700,700i,900,900i");
@import url("https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700,700i");
@import url("https://fonts.googleapis.com/css?family=Nunito:400,500,700");
body {
font-family: "Google Sans", "Roboto", sans-serif;
}
</style>
</head>
<body>${html}</body>
</html>
`);
doc.close();
}
}, [html]);

return (
<div ref={contRef} style={{ width: "100%", overflowY: "scroll", overflowX: "hidden", height: "100vh" }}>
<iframe
ref={iframeRef}
style={{ width: "100%", borderWidth: 0, overflowY: "scroll", overflowX: "hidden", height: "100vh" }}
/>
</div>
);
}

export default PBPublishedRender;
23 changes: 23 additions & 0 deletions src/components/Pages/Custom Pages/render/engine/blocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const Tags = {
div: { type: "div" },
img: { type: "img" },
p: { type: "p" },
span: { type: "span" },
h2: { type: "h2" },
video: { type: "iframe" },
link: { type: "a" },
icon: { type: "i" },
richtext: { type: "div" },
button: {
type: "a",
style: {
"border-radius": "4px",
cursor: "pointer",
background: "#9fddeb47",
border: "solid 0px #0b9edc",
padding: "10px 20px",
color: "#0b9edc",
"font-weight": "bold",
},
},
};
96 changes: 96 additions & 0 deletions src/components/Pages/Custom Pages/render/engine/engine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Tags } from "./blocks";
import React from "react";
import { serializeCss } from "./serialize-css";
const X = "x";
const Y = "y";
export const DIRECTIONS = { X, Y };

export function debounce(func, delay) {
let timeout;

return function (...args) {
clearTimeout(timeout); // Clear the previous timeout
timeout = setTimeout(() => {
func.apply(this, args); // Call the function after the delay
}, delay);
};
}
const layoutFlow = (direction, serialize = false) => {
// let directionKey = serialize ? "flex-direction" : "flexDirection";

return { display: "flex", flexDirection: direction === DIRECTIONS.X ? "row" : "column" };
};


export const serializeBlock = (block) => {
const { direction, element, content, children: childElements } = block || {};
const { type } = element || {};
const { text, style, ...props } = element?.props || {};
if (!element) return "";

// Determine the tag to use
const Tag = Tags[type]?.type || "div";
const defaultTagStyle = Tags[type]?.style || {};

// Convert style object to inline style string
const styleTogether = { ...defaultTagStyle, ...style, ...layoutFlow(direction, true) };
const styleString = serializeCss(styleTogether);

// Serialize props (excluding style and text)
const propsString = Object.entries(props || {})
.map(([key, value]) => `${key}="${value}"`)
.join(" ");

const isRich = type === "richtext";
const isVideo = type === "video";
const isButton = type === "button";

// If the block is rich text, return the inner HTML
const richText = `<div style="${styleString}"> ${props?.__html} </div>`;

if (isButton) {
const { alignItems, justifyContent, color, ...btnRest } = styleTogether || {};
const obj = { alignItems, justifyContent };
const rootStyles = serializeCss(obj);
const colorString = color ? `color:${color};` : "";
const btnRestString = serializeCss(btnRest);
return `<div style="width:100%;display:flex; flex-direction:column;${rootStyles}"><a style ="text-align:center;${colorString}${btnRestString}" ${propsString}>${text}</a></div>`;
}
if (isRich) return richText;
if (isVideo) return serializeVideoBlock({ src: props?.src, styleString: serializeCss(styleTogether), propsString });

// Serialize children recursively
const innerHTML =
content ||
(childElements &&
childElements
.map((el) => serializeBlock(el)) // Recursively serialize children
.join("")) ||
"";

// Serialize the block
return `<${Tag} ${styleString ? `style="${styleString}"` : ""} ${propsString}>
${text || ""}
${innerHTML}
</${Tag}>`;
};

const serializeVideoBlock = ({ src, styleString, propsString }) => {
return `
<div>
<iframe
src = "https://www.youtube.com/embed/${src}"
title="YouTube video"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style="width:100%;border:none;${styleString}"
${propsString}
>
</iframe>
</div>
`;
};

// src={${src}}
// style="width:100%;height: auto;border: none;"
25 changes: 25 additions & 0 deletions src/components/Pages/Custom Pages/render/engine/serialize-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const INLINE_KEYS = {
alignItems: "align-items",
flexDirection: "flex-direction",
justifyContent: "justify-content",
flexWrap: "flex-wrap",
flexBasis: "flex-basis",
objectFit: "object-fit",
marginTop: "margin-top",
marginBottom: "margin-bottom",
marginLeft: "margin-left",
marginRight: "margin-right",
paddingTop: "padding-top",
paddingBottom: "padding-bottom",
paddingLeft: "padding-left",
paddingRight: "padding-right",
fontSize: "font-size",
};
export const serializeCss = (inLinObj) => {
return Object.entries(inLinObj)
.map(([key, value]) => {
key = INLINE_KEYS[key] || key;
return `${key}: ${value};`;
})
.join(" ");
};
2 changes: 1 addition & 1 deletion src/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"IS_LOCAL": false,
"IS_PROD": false,
"IS_CANARY": false,
"BUILD_VERSION": "4.14.8"
"BUILD_VERSION": "4.14.11"
}
83 changes: 83 additions & 0 deletions src/hooks/useApiRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { useState } from "react";
import { apiCall } from "../api/functions";

/**
*
* @param {*} objArrays
* @returns Array of objects(requestHandlers). Each of the handlers is an array containing the following:
* 1. apiRequest: Function to make the API request
* 2. data: The data returned from the API request
* 3. error: The error returned from the API request
* 4. loading: The loading state of the API request
* 5. response: The response returned from the API request
*
*/
export const useApiRequest = (objArrays) => {
const [loading, setLoading] = useState({});
const [data, setData] = useState({});
const [response, setResponse] = useState({});
const [error, setError] = useState({});

const handleResponse = (key, response) => {
setDataValue(key, response?.data);
setResponseValue(key, response);
};
const setLoadingValue = (key, value) => {
setLoading({ ...loading, [key]: value });
};
const setDataValue = (key, value) => {
setData({ ...data, [key]: value });
};
const setErrorValue = (key, value) => {
setError({ ...error, [key]: value });
};
const setResponseValue = (key, value) => {
setResponse({ ...response, [key]: value });
};

const apiRequest = (body, cb, options) => {
const { url, key } = options || {};
setLoadingValue(key, true);
setErrorValue(key, null);
apiCall(url, body)
.then((response) => {
setLoadingValue(key, false);
if (!response?.success) {
setErrorValue(key, response?.error);
}
handleResponse(key, response);
cb && cb(response);
})
.catch((e) => {
console.log(`useApiError: ${key} => `, e);
setLoadingValue(key, false);
setErrorValue(key, e?.toString());
cb && cb(null, e?.toString());
});
};

// return objArrays.map((obj) => {
// const key = obj?.key;
// return {
// error: error[key],
// loading: loading[key],
// apiRequest: (externalProps) => apiRequest({ ...obj, ...(externalProps || {}), key }),
// data: data[key],
// response: response[key]
// };
// });

return objArrays.map((obj) => {
const key = obj?.key;
return [
(body, cb, options = {}) => apiRequest(body, cb, { ...obj, ...(options || {}), key }),
data[key],
error[key],
loading[key],
(error) => setErrorValue(key, error),
(value) => setLoadingValue(key, value),
(data) => setDataValue(key, data),
response[key]
];
});
};

0 comments on commit 9303b8d

Please sign in to comment.