From f9c07e96f07a460388bfbd13b3c93f73ef013657 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 23 Jun 2023 12:37:00 -0400 Subject: [PATCH 01/59] feat(strapi-integration): Proof of concept: adds support for getting sidebar ads data from Strapi and displaying them in the right context --- static/js/Promotions.jsx | 415 +++++++++++++++++++++-------------- static/js/ReaderApp.jsx | 28 ++- static/js/context.js | 72 +++++- static/js/sefaria/sefaria.js | 1 + 4 files changed, 335 insertions(+), 181 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index 77e6640961..d132e0cdce 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -1,145 +1,211 @@ -import React, {useState, useContext, useEffect} from 'react'; -import { AdContext } from './context'; -import classNames from 'classnames'; -import { InterruptingMessage } from './Misc'; -import Sefaria from './sefaria/sefaria'; - -const Promotions = ({adType, rerender}) => { - const [inAppAds, setInAppAds] = useState(Sefaria._inAppAds); - const [matchingAds, setMatchingAds] = useState(null); - const [prevMatchingAdIds, setPrevMatchingAdIds] = useState([]) - const context = useContext(AdContext); - useEffect(() => { - google.charts.load("current"); - google.charts.setOnLoadCallback(getAds) - }, []); - useEffect(() => { - if(inAppAds) { - setMatchingAds(getCurrentMatchingAds()); - } - }, [context, inAppAds]); - useEffect(() => { - if (!matchingAds) {return} - const matchingAdIds = matchingAds.map(x => x.campaignId).sort(); - const newIds = matchingAdIds.filter(value=> !(prevMatchingAdIds.includes(value))); - - if (newIds.length > 0) { - for (const matchingAd of matchingAds) { - if (newIds.includes(matchingAd.campaignId)) { - gtag("event", "promo_viewed", { - campaignID: matchingAd.campaignId, - adType:matchingAd.adType - }) - } - } - setPrevMatchingAdIds(newIds) - } - }, [matchingAds]) - - function getAds() { - const url = - 'https://docs.google.com/spreadsheets/d/1UJw2Akyv3lbLqBoZaFVWhaAp-FUQ-YZfhprL_iNhhQc/edit#gid=0' - const query = new google.visualization.Query(url); - query.setQuery('select A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q'); - query.send(processSheetsData); +import React, { useState, useContext, useEffect } from "react"; +import { AdContext, StrapiDataProvider, StrapiDataContext } from "./context"; +import classNames from "classnames"; +import { InterruptingMessage } from "./Misc"; +import Sefaria from "./sefaria/sefaria"; + +const Promotions = ({ adType, rerender }) => { + const [inAppAds, setInAppAds] = useState(Sefaria._inAppAds); // local cache + const [matchingAds, setMatchingAds] = useState(null); // match the ads to what comes from the google doc + const [prevMatchingAdIds, setPrevMatchingAdIds] = useState([]); + const context = useContext(AdContext); + const strapi = useContext(StrapiDataContext); + useEffect(() => { + if (strapi.dataFromStrapiHasBeenReceived) { + Sefaria._inAppAds = []; + console.log("we got some data"); + console.log(JSON.stringify(strapi.strapiData, null, 2)); + + const sidebarAds = strapi.strapiData.sidebarAds.data; + + sidebarAds.forEach((sidebarAd) => { + sidebarAd = sidebarAd.attributes; + console.log(JSON.stringify(sidebarAd, null, 2)); + let keywordTargetsArray = sidebarAd.keywords + .split(",") + .map((x) => x.trim().toLowerCase()); + Sefaria._inAppAds.push({ + campaignId: sidebarAd.internalCampaignId, + title: sidebarAd.Title, + bodyText: sidebarAd.bodyText, + buttonText: sidebarAd.buttonText, + buttonUrl: sidebarAd.buttonUrl, + buttonIcon: "", + buttonLocation: sidebarAd.buttonUrl, + adType: "sidebar", + hasBlueBackground: sidebarAd.hasBlueBackground, + repetition: 5, + buttonStyle: "", + trigger: { + showTo: sidebarAd.showTo, + interfaceLang: Sefaria.translateISOLanguageCode( + sidebarAd.locale + ).toLowerCase(), + dt_start: Date.parse(sidebarAd.startTime), + dt_end: Date.parse(sidebarAd.endTime), + keywordTargets: keywordTargetsArray, + excludeKeywordTargets: [], + }, + debug: sidebarAd.debug, + }); + }); + setInAppAds(Sefaria._inAppAds); + } + }, [strapi.dataFromStrapiHasBeenReceived]); + // empty array happens when the page loads, equivalent of didcomponentmount + // dataFromStrapiHasBeenReceived will originally be null until that part is scheduled and executed + useEffect(() => { + if (inAppAds) { + setMatchingAds(getCurrentMatchingAds()); + } + }, [context, inAppAds]); // when state changes, the effect will run + useEffect(() => { + if (!matchingAds) { + return; } + const matchingAdIds = matchingAds.map((x) => x.campaignId).sort(); + const newIds = matchingAdIds.filter( + (value) => !prevMatchingAdIds.includes(value) + ); - function showToUser(ad) { - if (ad.trigger.showTo === "all") { - return true; - } else if (ad.trigger.showTo === "loggedIn" && context.isLoggedIn) { - return true; - } else if (ad.trigger.showTo === "loggedOut" && !context.isLoggedIn) { - return true; - } else { - return false; + if (newIds.length > 0) { + for (const matchingAd of matchingAds) { + if (newIds.includes(matchingAd.campaignId)) { + gtag("event", "promo_viewed", { + campaignID: matchingAd.campaignId, + adType: matchingAd.adType, + }); } + } + setPrevMatchingAdIds(newIds); } + }, [matchingAds]); // when state of matching ads changes, which changes in previous useEffect - function showGivenDebugMode(ad) { - if (!ad.debug) { - return true; - } else if (context.isDebug == true) { - return true; - } else { - return false - } + // function getAds() { + // const url = + // 'https://docs.google.com/spreadsheets/d/1UJw2Akyv3lbLqBoZaFVWhaAp-FUQ-YZfhprL_iNhhQc/edit#gid=0' + // const query = new google.visualization.Query(url); + // query.setQuery('select A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q'); + // query.send(processSheetsData); + // + // + // } + + function showToUser(ad) { + if (ad.trigger.showTo === "all") { + return true; + } else if (ad.trigger.showTo === "loggedIn" && context.isLoggedIn) { + return true; + } else if (ad.trigger.showTo === "loggedOut" && !context.isLoggedIn) { + return true; + } else { + return false; + } + } + + function showGivenDebugMode(ad) { + if (!ad.debug) { + return true; + } else if (context.isDebug == true) { + return true; + } else { + return false; } - - + } + function getCurrentMatchingAds() { // TODO: refine matching algorithm to order by matchingness? - return inAppAds.filter(ad => { + return inAppAds.filter((ad) => { return ( showToUser(ad) && showGivenDebugMode(ad) && ad.trigger.interfaceLang === context.interfaceLang && ad.adType === adType && - context.dt > ad.trigger.dt_start && context.dt < ad.trigger.dt_end && - (context.keywordTargets.some(kw => ad.trigger.keywordTargets.includes(kw)) || - (ad.trigger.excludeKeywordTargets.length !== 0 && !context.keywordTargets.some(kw => ad.trigger.excludeKeywordTargets.includes(kw)))) && + context.dt > ad.trigger.dt_start && + context.dt < ad.trigger.dt_end && + (context.keywordTargets.some((kw) => + ad.trigger.keywordTargets.includes(kw) + ) || + (ad.trigger.excludeKeywordTargets.length !== 0 && + !context.keywordTargets.some((kw) => + ad.trigger.excludeKeywordTargets.includes(kw) + ))) && /* line below checks if ad with particular repetition number has been seen before and is a banner */ - (Sefaria._inBrowser && !document.cookie.includes(`${ad.campaignId}_${ad.repetition}`) || ad.adType === "sidebar") - ) - }) + ((Sefaria._inBrowser && + !document.cookie.includes(`${ad.campaignId}_${ad.repetition}`)) || + ad.adType === "sidebar") + ); + }); } - function processSheetsData(response) { - if (response.isError()) { - alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage()); - return; - } - const data = response.getDataTable(); - const columns = data.getNumberOfColumns(); - const rows = data.getNumberOfRows(); - Sefaria._inAppAds = []; - for (let r = 0; r < rows; r++) { - let row = []; - for (let c = 0; c < columns; c++) { - row.push(data.getFormattedValue(r, c)); - } - let keywordTargetsArray = row[5].split(",").map(x => x.trim().toLowerCase()); - let excludeKeywordTargets = keywordTargetsArray.filter(x => x.indexOf("!") === 0); - excludeKeywordTargets = excludeKeywordTargets.map(x => x.slice(1)); - keywordTargetsArray = keywordTargetsArray.filter(x => x.indexOf("!") !== 0) - Sefaria._inAppAds.push( - { - campaignId: row[0], - title: row[6], - bodyText: row[7], - buttonText: row[8], - buttonUrl: row[9], - buttonIcon: row[10], - buttonLocation: row[11], - adType: row[12], - hasBlueBackground: parseInt(row[13]), - repetition: row[14], - buttonStyle: row[15], - trigger: { - showTo: row[4] , - interfaceLang: row[3], - dt_start: Date.parse(row[1]), - dt_end: Date.parse(row[2]), - keywordTargets: keywordTargetsArray, - excludeKeywordTargets: excludeKeywordTargets - }, - debug: parseInt(row[16]) - } - ) + function processSheetsData(response) { + if (response.isError()) { + alert( + "Error in query: " + + response.getMessage() + + " " + + response.getDetailedMessage() + ); + return; + } + const data = response.getDataTable(); + const columns = data.getNumberOfColumns(); + const rows = data.getNumberOfRows(); + Sefaria._inAppAds = []; + for (let r = 0; r < rows; r++) { + let row = []; + for (let c = 0; c < columns; c++) { + row.push(data.getFormattedValue(r, c)); } - setInAppAds(Sefaria._inAppAds); - + let keywordTargetsArray = row[5] + .split(",") + .map((x) => x.trim().toLowerCase()); + let excludeKeywordTargets = keywordTargetsArray.filter( + (x) => x.indexOf("!") === 0 + ); + excludeKeywordTargets = excludeKeywordTargets.map((x) => x.slice(1)); + keywordTargetsArray = keywordTargetsArray.filter( + (x) => x.indexOf("!") !== 0 + ); + Sefaria._inAppAds.push({ + campaignId: row[0], + title: row[6], + bodyText: row[7], + buttonText: row[8], + buttonUrl: row[9], + buttonIcon: row[10], + buttonLocation: row[11], + adType: row[12], + hasBlueBackground: parseInt(row[13]), + repetition: row[14], + buttonStyle: row[15], + trigger: { + showTo: row[4], + interfaceLang: row[3], + dt_start: Date.parse(row[1]), + dt_end: Date.parse(row[2]), + keywordTargets: keywordTargetsArray, + excludeKeywordTargets: excludeKeywordTargets, + }, + debug: parseInt(row[16]), + }); } + setInAppAds(Sefaria._inAppAds); + } - // TODO: refactor once old InterruptingMessage pattern is retired - function createBannerHtml(matchingAd) { - return `
- + // TODO: refactor once old InterruptingMessage pattern is retired + function createBannerHtml(matchingAd) { + return `
+ ${matchingAd.bodyText}
- -
- {interruptingMessage} -
- {header} - {panels} - {sefariaModal} - {communityPagePreviewControls} - + + +
+ {interruptingMessage} +
+ {header} + {panels} + {sefariaModal} + {communityPagePreviewControls} + {beitMidrashPanel} + + {/* */} +
-
- + + ); } } diff --git a/static/js/context.js b/static/js/context.js index d21b68b8d4..4387d1ac2c 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -1,14 +1,72 @@ -import React from 'react'; +import React, { useContext, useEffect, useState } from "react"; const ContentLanguageContext = React.createContext({ language: "english", }); -ContentLanguageContext.displayName = 'ContentLanguageContext'; //This lets us see this name in the devtools +ContentLanguageContext.displayName = "ContentLanguageContext"; //This lets us see this name in the devtools -const AdContext = React.createContext({ -}); -AdContext.displayName = 'AdContext'; +const AdContext = React.createContext({}); +AdContext.displayName = "AdContext"; + +const StrapiDataContext = React.createContext({}); +StrapiDataContext.displayName = "StrapiDataContext"; + +function StrapiDataProvider({ children }) { + const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = + useState(false); + const [strapiData, setStrapiData] = useState(null); + useEffect(() => { + const getStrapiData = async () => { + try { + const result = fetch("http://localhost:1337/graphql", { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + body: '{"query":"# Write your query or mutation here\\nquery {\\n sidebarAds(\\n filters: {\\n startTime: { gte: \\"2023-06-19T04:00:00.000Z\\" }\\n and: [{ endTime: { lte: \\"2023-06-29T04:00:00.000Z\\" } }]\\n }\\n ) {\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}\\n"}', + }) + .then((response) => response.json()) + .then((result) => { + setStrapiData(result.data); + setDataFromStrapiHasBeenReceived(true); + }); + } catch (error) { + console.error("Failed to get strapi data", error); + } + }; + getStrapiData(); + }, []); + + return ( + + {children} + + ); +} + +// function ExampleComponent() { +// const strapi = useContext(StrapiDataContext); +// if (strapi.dataFromStrapiHasBeenReceived) { +// return ( +//
+// {strapi.strapiData} +//
+// ); +// } else { +// return null; +// } +// } export { ContentLanguageContext, - AdContext -}; \ No newline at end of file + AdContext, + StrapiDataProvider, + // ExampleComponent, + StrapiDataContext, +}; diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index 9698478df8..2cd4fee1b2 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -2318,6 +2318,7 @@ _media: {}, }, _tableOfContentsDedications: {}, + _strapiContent: null, _inAppAds: null, _stories: { stories: [], From b78902ea92ce0fb17b161ade7db38f160d02433e Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 7 Jul 2023 11:17:08 -0400 Subject: [PATCH 02/59] First rough draft for updating the interruptingmessage component to support using Strapi for modals --- sefaria/settings.py | 20 +- static/js/Misc.jsx | 411 +++++++++++++++++++++++++++++++--------- static/js/ReaderApp.jsx | 58 +++--- static/js/context.js | 38 +++- 4 files changed, 406 insertions(+), 121 deletions(-) diff --git a/sefaria/settings.py b/sefaria/settings.py index 38229af2ca..40d54c4278 100644 --- a/sefaria/settings.py +++ b/sefaria/settings.py @@ -299,20 +299,32 @@ +# GLOBAL_INTERRUPTING_MESSAGE = { +# "name": "2023-06-16-help-center", +# "style": "banner", # "modal" or "banner" +# "repetition": 1, +# "is_fundraising": False, +# "condition": { +# "returning_only": False, +# "english_only": False, +# "desktop_only": True, +# "debug": False, +# } +# } + GLOBAL_INTERRUPTING_MESSAGE = { - "name": "2023-08-08-anniversary", + "name": "2022-04-07-passover-donate-modal", "style": "modal", # "modal" or "banner" "repetition": 1, "is_fundraising": False, "condition": { "returning_only": False, "english_only": False, - "desktop_only": False, - "debug": False, + "desktop_only": True, + "debug": True, } } - # GLOBAL_INTERRUPTING_MESSAGE = None diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index e20562c887..45d5a2c845 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -7,7 +7,8 @@ import Sefaria from './sefaria/sefaria'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import Component from 'react-class'; -import {usePaginatedDisplay} from './Hooks'; +import { usePaginatedDisplay } from './Hooks'; +import {ContentLanguageContext, AdContext, StrapiDataContext} from './context'; import ReactCrop from 'react-image-crop'; import 'react-image-crop/dist/ReactCrop.css'; import {ContentText} from "./ContentText"; @@ -2057,109 +2058,337 @@ SignUpModal.propTypes = { }; -class InterruptingMessage extends Component { - constructor(props) { - super(props); - this.displayName = 'InterruptingMessage'; - this.state = { - timesUp: false, - animationStarted: false +// class InterruptingMessage extends Component { +// constructor(props) { +// super(props); +// this.displayName = 'InterruptingMessage'; +// this.state = { +// timesUp: false, +// animationStarted: false +// }; +// this.settings = { +// "modal": { +// "trackingName": "Interrupting Message", +// "showDelay": 10000, +// }, +// "banner": { +// "trackingName": "Banner Message", +// "showDelay": 1, +// } +// }[this.props.style]; +// } +// componentDidMount() { +// if (this.shouldShow()) { +// this.delayedShow(); +// } +// } +// shouldShow() { +// const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; +// return excludedPaths.indexOf(window.location.pathname) === -1; +// } +// delayedShow() { +// setTimeout(function() { +// this.setState({timesUp: true}); +// $("#interruptingMessage .button").click(this.close); +// $("#interruptingMessage .trackedAction").click(this.trackAction); +// this.showAorB(); +// this.animateOpen(); +// }.bind(this), this.settings.showDelay); +// } +// animateOpen() { +// setTimeout(function() { +// if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").addClass("hasBannerMessage"); } +// this.setState({animationStarted: true}); +// this.trackOpen(); +// }.bind(this), 50); +// } +// showAorB() { +// // Allow random A/B testing if items are tagged ".optionA", ".optionB" +// const $message = $(ReactDOM.findDOMNode(this)); +// if ($message.find(".optionA").length) { +// console.log("rand show") +// Math.random() > 0.5 ? $(".optionA").show() : $(".optionB").show(); +// } +// } +// close() { +// this.markAsRead(); +// this.props.onClose(); +// // if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").removeClass("hasBannerMessage"); } +// } +// trackOpen() { +// Sefaria.track.event(this.settings.trackingName, "open", this.props.messageName, { nonInteraction: true }); +// } +// trackAction() { +// Sefaria.track.event(this.settings.trackingName, "action", this.props.messageName, { nonInteraction: true }); +// } +// markAsRead() { +// Sefaria._api("/api/interrupting-messages/read/" + this.props.messageName, function (data) {}); +// var cookieName = this.props.messageName + "_" + this.props.repetition; +// $.cookie(cookieName, true, { path: "/", expires: 14 }); +// Sefaria.track.event(this.settings.trackingName, "read", this.props.messageName, { nonInteraction: true }); +// Sefaria.interruptingMessage = null; +// } +// render() { +// if (!this.state.timesUp) { return null; } + +// if (this.props.style === "banner") { +// return
+//
+//
×
+//
; + +// } else if (this.props.style === "modal") { +// return
+//
+//
+//
+//
+//
×
+//
+// {/*
*/} +//
+//
+//
+//
; +// } +// return null; +// } +// } +// InterruptingMessage.propTypes = { +// messageName: PropTypes.string.isRequired, +// messageHTML: PropTypes.string.isRequired, +// style: PropTypes.string.isRequired, +// repetition: PropTypes.number.isRequired, // manual toggle to refresh an existing message +// onClose: PropTypes.func.isRequired +// }; + +export function useIsVisible(ref) { + const [isIntersecting, setIntersecting] = useState(false); + console.log(ref); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => setIntersecting(entry.isIntersecting), + { threshhold: 1.0 } + ); + + if (ref.current) observer.observe(ref.current); + return () => { + observer.disconnect(); }; - this.settings = { - "modal": { - "trackingName": "Interrupting Message", - "showDelay": 1000, + }, [ref]); + + return isIntersecting; +} + +function OnInView({ children, onVisible }) { + const elementRef = useRef(); + + useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + const [entry] = entries; + if (entry.isIntersecting) { + onVisible(); + } }, - "banner": { - "trackingName": "Banner Message", - "showDelay": 1, - } - }[this.props.style]; - } - componentDidMount() { - if (this.shouldShow()) { - this.delayedShow(); + { threshold: 1 } + ); + + if (elementRef.current) { + observer.observe(elementRef.current); } - } - shouldShow() { + + return () => { + if (elementRef.current) { + observer.unobserve(elementRef.current); + } + }; + }, [onVisible]); + + return
{children}
; +} + + +const InterruptingMessage = ({ + messageName, + messageHTML, + style, + repetition, + onClose, +}) => { + const [timesUp, setTimesUp] = useState(false); + const [animationStarted, setAnimationStarted] = useState(false); + const [hasSeenModal, setHasSeenModal] = useState(false); + const strapi = useContext(StrapiDataContext); + + const ref = useRef(); + const isVisible = useIsVisible(ref); + + const settings = { + trackingName: "Interrupting Message", + showDelay: 5000, + }; + + // Need to figure out caching for Strapi so multiple queries aren't made on different page loads + // Use user context to determine whether this is valid for a user? + // Maybe user context should be used to find if there's a compatible modal + const shouldShow = () => { + if (!strapi.interruptingMessageModal) return false; const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; return excludedPaths.indexOf(window.location.pathname) === -1; + }; + + const closeModal = () => { + if (onClose) onClose(); + // Mark as read with cookies and track closing actions + setHasSeenModal(true); // should be acknolwedge instead of seen because there was an interaction + // Sefaria.interruptingMessageModal = null; } - delayedShow() { - setTimeout(function() { - this.setState({timesUp: true}); - $("#interruptingMessage .button").click(this.close); - $("#interruptingMessage .trackedAction").click(this.trackAction); - this.showAorB(); - this.animateOpen(); - }.bind(this), this.settings.showDelay); - } - animateOpen() { - setTimeout(function() { - if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").addClass("hasBannerMessage"); } - this.setState({animationStarted: true}); - this.trackOpen(); - }.bind(this), 50); - } - showAorB() { - // Allow random A/B testing if items are tagged ".optionA", ".optionB" - const $message = $(ReactDOM.findDOMNode(this)); - if ($message.find(".optionA").length) { - console.log("rand show") - Math.random() > 0.5 ? $(".optionA").show() : $(".optionB").show(); - } - } - close() { - this.markAsRead(); - this.props.onClose(); - if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").removeClass("hasBannerMessage"); } - } - trackOpen() { - Sefaria.track.event(this.settings.trackingName, "open", this.props.messageName, { nonInteraction: true }); - } - trackAction() { - Sefaria.track.event(this.settings.trackingName, "action", this.props.messageName, { nonInteraction: true }); - } - markAsRead() { - Sefaria._api("/api/interrupting-messages/read/" + this.props.messageName, function (data) {}); - var cookieName = this.props.messageName + "_" + this.props.repetition; - $.cookie(cookieName, true, { path: "/", expires: 14 }); - Sefaria.track.event(this.settings.trackingName, "read", this.props.messageName, { nonInteraction: true }); - Sefaria.interruptingMessage = null; + + const trackImpression = () => { + console.log("We've got visibility!"); + // track impression here } - render() { - if (!this.state.timesUp) { return null; } - - if (this.props.style === "banner") { - return
-
-
×
-
; - - } else if (this.props.style === "modal") { - return
-
-
-
-
-
×
-
-
+ + useEffect(() => { + if (shouldShow()) { + const timeoutId = setTimeout(() => { + setTimesUp(true); + // Other stuff here + }, settings.showDelay); + return () => clearTimeout(timeoutId); // clearTimeout on component unmount + } + }, [strapi.interruptingMessageModal, settings.showDelay]); // execute useEffect when the modal or showDelay changes + + // useEffect(() => { + // if (timesUp) { + // const timeoutId = setTimeout(() => { + // // Track open action + // setAnimationStarted(true); + // }, 50); + // return () => clearTimeout(timeoutId); // clearTimeout on component unmount + // } + // }, [timesUp]); // execute useEffect when timesUp changes + + if (!timesUp) return null; + console.log("data for the component"); + console.log(strapi.interruptingMessageModal); + + if (!hasSeenModal) { + console.log("rendering component") + return ( + +
+
+ + ); + } else { return null; } -} -InterruptingMessage.propTypes = { - messageName: PropTypes.string.isRequired, - messageHTML: PropTypes.string.isRequired, - style: PropTypes.string.isRequired, - repetition: PropTypes.number.isRequired, // manual toggle to refresh an existing message - onClose: PropTypes.func.isRequired }; +// InterruptingMessage.propTypes = { +// messageName: PropTypes.string.isRequired, +// messageHTML: PropTypes.string.isRequired, +// style: PropTypes.string.isRequired, +// repetition: PropTypes.number.isRequired, +// onClose: PropTypes.func.isRequired +// }; + const NBox = ({ content, n, stretch, gap=0 }) => { // Wrap a list of elements into an n-column flexbox diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 1ea540c751..6982956b97 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -9,7 +9,7 @@ import $ from './sefaria/sefariaJquery'; import EditCollectionPage from './EditCollectionPage'; import Footer from './Footer'; import SearchState from './sefaria/searchState'; -import {ContentLanguageContext, AdContext, StrapiDataProvider, ExampleComponent} from './context'; +import {ContentLanguageContext, AdContext, StrapiDataProvider, ExampleComponent, StrapiDataContext} from './context'; import { ContestLandingPage, RemoteLearningPage, @@ -1961,6 +1961,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { .flat() .filter(ref => !!ref); const deDupedTriggers = [...new Set(triggers.map(JSON.stringify))].map(JSON.parse).map(x => x.toLowerCase()); + // How do I get the user type? const context = { isDebug: this.props._debug, isLoggedIn: Sefaria._uid, @@ -2151,13 +2152,13 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { {panels}
) : null; - var interruptingMessage = Sefaria.interruptingMessage ? - () : ; + // var interruptingMessage = Sefaria.interruptingMessage ? + // () : ; const sefariaModal = ( - -
- {interruptingMessage} -
- {header} - {panels} - {sefariaModal} - {communityPagePreviewControls} - {beitMidrashPanel} - - {/* */} -
-
-
- + + +
+ +
+ {header} + {panels} + {sefariaModal} + {communityPagePreviewControls} + {beitMidrashPanel} + + {/* */} +
+
+
+
); } } diff --git a/static/js/context.js b/static/js/context.js index 4387d1ac2c..d2e4fe4276 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -14,9 +14,25 @@ function StrapiDataProvider({ children }) { const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = useState(false); const [strapiData, setStrapiData] = useState(null); + const [interruptingMessageModal, setInterruptingMessageModal] = useState(null); useEffect(() => { const getStrapiData = async () => { try { + let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; + let getJSONDateStringInLocalTimeZone = (date) => { + let parts = getDateWithoutTime(date).split("-"); + return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); + }; + let currentDate = new Date(); + let oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(currentDate.getDate() + 7); + currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC + let startDate = getJSONDateStringInLocalTimeZone(currentDate); + let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); + console.log(startDate); + console.log(endDate); + const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; + console.log(query); const result = fetch("http://localhost:1337/graphql", { method: "POST", // *GET, POST, PUT, DELETE, etc. mode: "cors", // no-cors, *cors, same-origin @@ -27,12 +43,30 @@ function StrapiDataProvider({ children }) { }, redirect: "follow", // manual, *follow, error referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: '{"query":"# Write your query or mutation here\\nquery {\\n sidebarAds(\\n filters: {\\n startTime: { gte: \\"2023-06-19T04:00:00.000Z\\" }\\n and: [{ endTime: { lte: \\"2023-06-29T04:00:00.000Z\\" } }]\\n }\\n ) {\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}\\n"}', + body: query, }) .then((response) => response.json()) .then((result) => { setStrapiData(result.data); setDataFromStrapiHasBeenReceived(true); + // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists + // e.g. there are modals with overlapping time frames + let modals = result.data?.modals?.data; + console.log(modals); + const currentDate = new Date(); + if (modals?.length) { + // if they end up being sorted, the first one will be the compatible one + let modal = modals.find(modal => + currentDate >= new Date(modal.attributes.modalStartDate) && + currentDate <= new Date(modal.attributes.modalEndDate) + ); + console.log("found acceptable modal:"); + console.log(modal); + if (modal) { + console.log("setting the modal"); + setInterruptingMessageModal(modal.attributes); + } + } }); } catch (error) { console.error("Failed to get strapi data", error); @@ -43,7 +77,7 @@ function StrapiDataProvider({ children }) { return ( {children} From 46fb9648381ca4e9bbb1d5630b1ceb6338cb7476 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 10 Jul 2023 11:30:12 -0400 Subject: [PATCH 03/59] Start of rough draft for handling new visitors and returning visitors and determining whether the modal has been seen or not --- static/js/Misc.jsx | 166 +++++++++++++++++++++++++------------------ static/js/context.js | 2 +- 2 files changed, 98 insertions(+), 70 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 45d5a2c845..0142a4c4b8 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2162,24 +2162,6 @@ SignUpModal.propTypes = { // onClose: PropTypes.func.isRequired // }; -export function useIsVisible(ref) { - const [isIntersecting, setIntersecting] = useState(false); - console.log(ref); - - useEffect(() => { - const observer = new IntersectionObserver( - ([entry]) => setIntersecting(entry.isIntersecting), - { threshhold: 1.0 } - ); - - if (ref.current) observer.observe(ref.current); - return () => { - observer.disconnect(); - }; - }, [ref]); - - return isIntersecting; -} function OnInView({ children, onVisible }) { const elementRef = useRef(); @@ -2209,6 +2191,24 @@ function OnInView({ children, onVisible }) { return
{children}
; } +// let isNewVisitor = JSON.parse(localStorage.getItem("isNewVisitor")); +function isNewVisitor() { + return ( + "isNewVisitor" in sessionStorage || + ("isNewVisitor" in localStorage && + JSON.parse(localStorage.getItem("isNewVisitor"))) + ); +} + +function isReturningVisitor() { + return ( + !isNewVisitor() && + "isReturningVisitor" in localStorage && + JSON.parse(localStorage.getItem("isReturningVisitor")) + ); +} +let shouldShowModal = false; + const InterruptingMessage = ({ messageName, @@ -2218,16 +2218,11 @@ const InterruptingMessage = ({ onClose, }) => { const [timesUp, setTimesUp] = useState(false); - const [animationStarted, setAnimationStarted] = useState(false); const [hasSeenModal, setHasSeenModal] = useState(false); const strapi = useContext(StrapiDataContext); - - const ref = useRef(); - const isVisible = useIsVisible(ref); - const settings = { trackingName: "Interrupting Message", - showDelay: 5000, + showDelay: 500, }; // Need to figure out caching for Strapi so multiple queries aren't made on different page loads @@ -2235,23 +2230,64 @@ const InterruptingMessage = ({ // Maybe user context should be used to find if there's a compatible modal const shouldShow = () => { if (!strapi.interruptingMessageModal) return false; + if (!shouldShowModal) return false; const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; - return excludedPaths.indexOf(window.location.pathname) === -1; + return !JSON.parse(sessionStorage.getItem('modal_' + strapi.interruptingMessageModal.internalModalName)) && excludedPaths.indexOf(window.location.pathname) === -1; }; const closeModal = () => { if (onClose) onClose(); // Mark as read with cookies and track closing actions + sessionStorage.setItem('modal_' + strapi.interruptingMessageModal.internalModalName, 'true'); // maybe use modals as key and manipulate object entered setHasSeenModal(true); // should be acknolwedge instead of seen because there was an interaction // Sefaria.interruptingMessageModal = null; - } + }; const trackImpression = () => { console.log("We've got visibility!"); // track impression here - } + }; useEffect(() => { + if (Sefaria._uid) { + console.log("hitting logged in user"); + try { + localStorage.setItem("isNewVisitor", "false"); + sessionStorage.setItem("isNewVisitor", "false"); + localStorage.setItem("isReturningVisitor", "true"); + } catch { + shouldShowModal = true; + } + if (strapi.interruptingMessageModal?.showToReturningVisitors) { + shouldShowModal = true; + } + } else { + if (!isNewVisitor() && !isReturningVisitor()) { + console.log("not new visitor or returning visitor"); // first time here + try { + localStorage.setItem("isNewVisitor", "false"); + sessionStorage.setItem("isNewVisitor", "true"); + localStorage.setItem("isReturningVisitor", "true"); // This will make the current visitor a returning one once their session is cleared + // sessionStorage.setItem("isReturningVisitor", "false"); + } catch { + shouldShowModal = true; + } + } else if (isReturningVisitor()) { + console.log("returning visitor"); + if (strapi.interruptingMessageModal?.showToReturningVisitors) { + shouldShowModal = true; + } + } else if (isNewVisitor()) { + console.log("new visitor"); + if (strapi.interruptingMessageModal?.showToNewVisitors) { + shouldShowModal = true; + } + } + } + if (strapi.interruptingMessageModal && !strapi.interruptingMessageModal.showToNewVisitors && !strapi.interruptingMessageModal.showToReturningVisitors) { // Show to everyone if there is no state passed from Strapi + shouldShowModal = true; + } + if (shouldShow()) { const timeoutId = setTimeout(() => { setTimesUp(true); @@ -2261,40 +2297,26 @@ const InterruptingMessage = ({ } }, [strapi.interruptingMessageModal, settings.showDelay]); // execute useEffect when the modal or showDelay changes - // useEffect(() => { - // if (timesUp) { - // const timeoutId = setTimeout(() => { - // // Track open action - // setAnimationStarted(true); - // }, 50); - // return () => clearTimeout(timeoutId); // clearTimeout on component unmount - // } - // }, [timesUp]); // execute useEffect when timesUp changes - if (!timesUp) return null; console.log("data for the component"); console.log(strapi.interruptingMessageModal); if (!hasSeenModal) { - console.log("rendering component") + console.log("rendering component"); return ( -
-
-
); } else { diff --git a/static/js/context.js b/static/js/context.js index d2e4fe4276..4a0f6e599c 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -31,7 +31,7 @@ function StrapiDataProvider({ children }) { let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); console.log(startDate); console.log(endDate); - const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; + const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalModalName\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; console.log(query); const result = fetch("http://localhost:1337/graphql", { method: "POST", // *GET, POST, PUT, DELETE, etc. From a0ddafaf0ee1a358a60dffa8af3372b64427213f Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 10 Jul 2023 16:01:47 -0400 Subject: [PATCH 04/59] Adds event tracking for when a modal is viewed, when the close button is clicked, and when the donate button is clicked --- static/js/Misc.jsx | 51 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 0142a4c4b8..252e0ccbcc 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2175,7 +2175,7 @@ function OnInView({ children, onVisible }) { } }, { threshold: 1 } - ); + ); if (elementRef.current) { observer.observe(elementRef.current); @@ -2186,7 +2186,7 @@ function OnInView({ children, onVisible }) { observer.unobserve(elementRef.current); } }; - }, [onVisible]); + }, [onVisible]); return
{children}
; } @@ -2209,7 +2209,6 @@ function isReturningVisitor() { } let shouldShowModal = false; - const InterruptingMessage = ({ messageName, messageHTML, @@ -2222,7 +2221,7 @@ const InterruptingMessage = ({ const strapi = useContext(StrapiDataContext); const settings = { trackingName: "Interrupting Message", - showDelay: 500, + showDelay: 2000, }; // Need to figure out caching for Strapi so multiple queries aren't made on different page loads @@ -2232,20 +2231,36 @@ const InterruptingMessage = ({ if (!strapi.interruptingMessageModal) return false; if (!shouldShowModal) return false; const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; - return !JSON.parse(sessionStorage.getItem('modal_' + strapi.interruptingMessageModal.internalModalName)) && excludedPaths.indexOf(window.location.pathname) === -1; + return ( + !JSON.parse( + sessionStorage.getItem( + "modal_" + strapi.interruptingMessageModal.internalModalName + ) + ) && excludedPaths.indexOf(window.location.pathname) === -1 + ); }; - const closeModal = () => { + const closeModal = (eventDescription) => { if (onClose) onClose(); - // Mark as read with cookies and track closing actions - sessionStorage.setItem('modal_' + strapi.interruptingMessageModal.internalModalName, 'true'); // maybe use modals as key and manipulate object entered + sessionStorage.setItem( + "modal_" + strapi.interruptingMessageModal.internalModalName, + "true" + ); // maybe use modals as key and manipulate object entered + console.log(eventDescription); + gtag("event", "modal_interacted_with_" + eventDescription, { + campaignID: strapi.interruptingMessageModal.internalModalName, + adType: "modal", + }); setHasSeenModal(true); // should be acknolwedge instead of seen because there was an interaction // Sefaria.interruptingMessageModal = null; }; const trackImpression = () => { console.log("We've got visibility!"); - // track impression here + gtag("event", "modal_viewed", { + campaignID: strapi.interruptingMessageModal.internalModalName, + adType: "modal", + }); }; useEffect(() => { @@ -2284,7 +2299,12 @@ const InterruptingMessage = ({ } } } - if (strapi.interruptingMessageModal && !strapi.interruptingMessageModal.showToNewVisitors && !strapi.interruptingMessageModal.showToReturningVisitors) { // Show to everyone if there is no state passed from Strapi + if ( + strapi.interruptingMessageModal && + !strapi.interruptingMessageModal.showToNewVisitors && + !strapi.interruptingMessageModal.showToReturningVisitors + ) { + // Show to everyone if there is no state passed from Strapi shouldShowModal = true; } @@ -2310,7 +2330,12 @@ const InterruptingMessage = ({
-
+
{ + closeModal("close_clicked"); + }} + > ×
{/*
*/} @@ -2389,7 +2414,9 @@ const InterruptingMessage = ({ className="button int-en" target="_blank" href={strapi.interruptingMessageModal.buttonURL} - onClick={closeModal} + onClick={() => { + closeModal("donate_button_clicked"); + }} > {strapi.interruptingMessageModal.buttonText} From 1a8941236a145641583adae8bfe70da4cd29711b Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 11 Jul 2023 10:45:55 -0400 Subject: [PATCH 05/59] Adds support for determining whether a user is a sustainer in the front end and a demo for using it to determine whether a modal should be shown --- reader/views.py | 2 ++ static/js/Misc.jsx | 11 ++++++++++- static/js/sefaria/sefaria.js | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/reader/views.py b/reader/views.py index 00b42b4a22..c90003d34f 100644 --- a/reader/views.py +++ b/reader/views.py @@ -205,6 +205,7 @@ def base_props(request): "slug": profile.slug if profile else "", "is_moderator": request.user.is_staff, "is_editor": UserWrapper(user_obj=request.user).has_permission_group("Editors"), + "is_sustainer": profile.is_sustainer, "full_name": profile.full_name, "profile_pic_url": profile.profile_pic_url, "is_history_enabled": profile.settings.get("reading_history", True), @@ -226,6 +227,7 @@ def base_props(request): "slug": "", "is_moderator": False, "is_editor": False, + "is_sustainer": False, "full_name": "", "profile_pic_url": "", "is_history_enabled": True, diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 252e0ccbcc..b19e8963e7 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2251,7 +2251,7 @@ const InterruptingMessage = ({ campaignID: strapi.interruptingMessageModal.internalModalName, adType: "modal", }); - setHasSeenModal(true); // should be acknolwedge instead of seen because there was an interaction + setHasSeenModal(true); // should be interacted instead of seen because there was an interaction // Sefaria.interruptingMessageModal = null; }; @@ -2276,6 +2276,15 @@ const InterruptingMessage = ({ if (strapi.interruptingMessageModal?.showToReturningVisitors) { shouldShowModal = true; } + if (Sefaria.is_sustainer) { + console.log("we got ourselves a beautiful sustainer!"); + } + if (Sefaria.is_sustainer && strapi.interruptingMessageModal?.showToSustainers) { + shouldShowModal = true + } + else if (!Sefaria.is_sustainer && strapi.interruptingMessageModal?.showToNonSustainers) { + shouldShowModal = true; + } } else { if (!isNewVisitor() && !isReturningVisitor()) { console.log("not new visitor or returning visitor"); // first time here diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index 2cd4fee1b2..e9e17cdc6b 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -2959,6 +2959,7 @@ Sefaria.unpackBaseProps = function(props){ "slug", "is_moderator", "is_editor", + "is_sustainer", "full_name", "profile_pic_url", "is_history_enabled", From 98a7f2ea621e8aa78133b9232bc185d525e0ca65 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 12 Jul 2023 10:49:52 -0400 Subject: [PATCH 06/59] Reorganizes and refactors code to be cleaner. Also fixes a bug with determining whether a user is a new visitor or not --- static/js/Misc.jsx | 158 ++++++++++++++++++---------------------- static/js/ReaderApp.jsx | 11 +++ static/js/context.js | 16 ++-- 3 files changed, 92 insertions(+), 93 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index b19e8963e7..8573801002 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2162,7 +2162,7 @@ SignUpModal.propTypes = { // onClose: PropTypes.func.isRequired // }; - +// Write comments explaining how this works function OnInView({ children, onVisible }) { const elementRef = useRef(); @@ -2194,7 +2194,8 @@ function OnInView({ children, onVisible }) { // let isNewVisitor = JSON.parse(localStorage.getItem("isNewVisitor")); function isNewVisitor() { return ( - "isNewVisitor" in sessionStorage || + ("isNewVisitor" in sessionStorage && + JSON.parse(sessionStorage.getItem("isNewVisitor"))) || ("isNewVisitor" in localStorage && JSON.parse(localStorage.getItem("isNewVisitor"))) ); @@ -2207,7 +2208,6 @@ function isReturningVisitor() { JSON.parse(localStorage.getItem("isReturningVisitor")) ); } -let shouldShowModal = false; const InterruptingMessage = ({ messageName, @@ -2217,45 +2217,26 @@ const InterruptingMessage = ({ onClose, }) => { const [timesUp, setTimesUp] = useState(false); - const [hasSeenModal, setHasSeenModal] = useState(false); + const [hasInteractedWithModal, setHasInteractedWithModal] = useState(false); const strapi = useContext(StrapiDataContext); - const settings = { - trackingName: "Interrupting Message", - showDelay: 2000, + const showDelay = 5000; + + const markModalAsHasBeenInteractedWith = (modalName) => { + sessionStorage.setItem("modal_" + modalName, "true"); }; - // Need to figure out caching for Strapi so multiple queries aren't made on different page loads - // Use user context to determine whether this is valid for a user? - // Maybe user context should be used to find if there's a compatible modal - const shouldShow = () => { - if (!strapi.interruptingMessageModal) return false; - if (!shouldShowModal) return false; - const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; - return ( - !JSON.parse( - sessionStorage.getItem( - "modal_" + strapi.interruptingMessageModal.internalModalName - ) - ) && excludedPaths.indexOf(window.location.pathname) === -1 - ); + const hasModalBeenInteractedWith = (modalName) => { + return JSON.parse(sessionStorage.getItem("modal_" + modalName)); }; - const closeModal = (eventDescription) => { - if (onClose) onClose(); - sessionStorage.setItem( - "modal_" + strapi.interruptingMessageModal.internalModalName, - "true" - ); // maybe use modals as key and manipulate object entered - console.log(eventDescription); + const trackModalInteraction = (modalName, eventDescription) => { gtag("event", "modal_interacted_with_" + eventDescription, { - campaignID: strapi.interruptingMessageModal.internalModalName, + campaignID: modalName, adType: "modal", }); - setHasSeenModal(true); // should be interacted instead of seen because there was an interaction - // Sefaria.interruptingMessageModal = null; }; - const trackImpression = () => { + const trackModalImpression = () => { console.log("We've got visibility!"); gtag("event", "modal_viewed", { campaignID: strapi.interruptingMessageModal.internalModalName, @@ -2263,77 +2244,77 @@ const InterruptingMessage = ({ }); }; - useEffect(() => { - if (Sefaria._uid) { - console.log("hitting logged in user"); - try { - localStorage.setItem("isNewVisitor", "false"); - sessionStorage.setItem("isNewVisitor", "false"); - localStorage.setItem("isReturningVisitor", "true"); - } catch { - shouldShowModal = true; - } - if (strapi.interruptingMessageModal?.showToReturningVisitors) { - shouldShowModal = true; - } - if (Sefaria.is_sustainer) { - console.log("we got ourselves a beautiful sustainer!"); - } - if (Sefaria.is_sustainer && strapi.interruptingMessageModal?.showToSustainers) { - shouldShowModal = true - } - else if (!Sefaria.is_sustainer && strapi.interruptingMessageModal?.showToNonSustainers) { - shouldShowModal = true; - } - } else { - if (!isNewVisitor() && !isReturningVisitor()) { - console.log("not new visitor or returning visitor"); // first time here - try { - localStorage.setItem("isNewVisitor", "false"); - sessionStorage.setItem("isNewVisitor", "true"); - localStorage.setItem("isReturningVisitor", "true"); // This will make the current visitor a returning one once their session is cleared - // sessionStorage.setItem("isReturningVisitor", "false"); - } catch { - shouldShowModal = true; - } - } else if (isReturningVisitor()) { - console.log("returning visitor"); - if (strapi.interruptingMessageModal?.showToReturningVisitors) { - shouldShowModal = true; - } - } else if (isNewVisitor()) { - console.log("new visitor"); - if (strapi.interruptingMessageModal?.showToNewVisitors) { - shouldShowModal = true; - } - } - } + // Need to figure out caching for Strapi so multiple queries aren't made on different page loads + // Use user context to determine whether this is valid for a user? + // Maybe user context should be used to find if there's a compatible modal + const shouldShow = () => { + if (!strapi.interruptingMessageModal) return false; if ( - strapi.interruptingMessageModal && - !strapi.interruptingMessageModal.showToNewVisitors && - !strapi.interruptingMessageModal.showToReturningVisitors - ) { - // Show to everyone if there is no state passed from Strapi + hasModalBeenInteractedWith( + strapi.interruptingMessageModal.internalModalName + ) + ) + return false; + + let shouldShowModal = false; + + let noUserKindIsSet = ![ + strapi.interruptingMessageModal.showToReturningVisitors, + strapi.interruptingMessageModal.showToNewVisitors, + strapi.interruptingMessageModal.showToSustainers, + strapi.interruptingMessageModal.showToNonSustainers, + ].some((p) => p); + if ( + Sefaria._uid && + ((Sefaria.is_sustainer && + strapi.interruptingMessageModal.showToSustainers) || + (!Sefaria.is_sustainer && + strapi.interruptingMessageModal.showToNonSustainers)) + ) shouldShowModal = true; - } + else if ( + (isReturningVisitor() && + strapi.interruptingMessageModal.showToReturningVisitors) || + (isNewVisitor() && strapi.interruptingMessageModal.showToNewVisitors) + ) + shouldShowModal = true; + else if (noUserKindIsSet) shouldShowModal = true; + if (!shouldShowModal) return false; + const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; + return excludedPaths.indexOf(window.location.pathname) === -1; + }; + + const closeModal = (eventDescription) => { + if (onClose) onClose(); + console.log(eventDescription); + markModalAsHasBeenInteractedWith( + strapi.interruptingMessageModal.internalModalName + ); + setHasInteractedWithModal(true); + trackModalInteraction( + strapi.interruptingMessageModal.internalModalName, + eventDescription + ); + }; + + useEffect(() => { if (shouldShow()) { const timeoutId = setTimeout(() => { setTimesUp(true); - // Other stuff here - }, settings.showDelay); + }, showDelay); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } - }, [strapi.interruptingMessageModal, settings.showDelay]); // execute useEffect when the modal or showDelay changes + }, [strapi.interruptingMessageModal]); // execute useEffect when the modal changes if (!timesUp) return null; console.log("data for the component"); console.log(strapi.interruptingMessageModal); - if (!hasSeenModal) { + if (!hasInteractedWithModal) { console.log("rendering component"); return ( - +
@@ -2444,6 +2425,7 @@ const InterruptingMessage = ({ return null; } }; +InterruptingMessage.displayName = "InterruptingMessage"; // InterruptingMessage.propTypes = { // messageName: PropTypes.string.isRequired, diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 6982956b97..7aadfded42 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -199,6 +199,17 @@ class ReaderApp extends Component { document.addEventListener('click', this.handleInAppClickWithModifiers, {capture: true}); // Save all initial panels to recently viewed this.state.panels.map(this.saveLastPlace); + // Initialize entries for first-time visitors to determine if they are new or returning + if (!("isNewVisitor" in localStorage) && !("isReturningVisitor" in localStorage)) { + sessionStorage.setItem("isNewVisitor", "true"); + // Setting these at this time will make the current new visitor a returning one once their session is cleared + localStorage.setItem("isNewVisitor", "false"); + localStorage.setItem("isReturningVisitor", "true"); + } else if (Sefaria._uid) { + localStorage.setItem("isNewVisitor", "false"); + sessionStorage.setItem("isNewVisitor", "false"); + localStorage.setItem("isReturningVisitor", "true"); + } } componentWillUnmount() { window.removeEventListener("popstate", this.handlePopState); diff --git a/static/js/context.js b/static/js/context.js index 4a0f6e599c..790cf305fa 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -14,7 +14,8 @@ function StrapiDataProvider({ children }) { const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = useState(false); const [strapiData, setStrapiData] = useState(null); - const [interruptingMessageModal, setInterruptingMessageModal] = useState(null); + const [interruptingMessageModal, setInterruptingMessageModal] = + useState(null); useEffect(() => { const getStrapiData = async () => { try { @@ -56,9 +57,10 @@ function StrapiDataProvider({ children }) { const currentDate = new Date(); if (modals?.length) { // if they end up being sorted, the first one will be the compatible one - let modal = modals.find(modal => - currentDate >= new Date(modal.attributes.modalStartDate) && - currentDate <= new Date(modal.attributes.modalEndDate) + let modal = modals.find( + (modal) => + currentDate >= new Date(modal.attributes.modalStartDate) && + currentDate <= new Date(modal.attributes.modalEndDate) ); console.log("found acceptable modal:"); console.log(modal); @@ -77,7 +79,11 @@ function StrapiDataProvider({ children }) { return ( {children} From 56b69c8e1a83419e56a588dcb092ec90c62b93f6 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 13 Jul 2023 14:02:05 -0400 Subject: [PATCH 07/59] Remove cruft of props passed to InterruptingMessage component since it will be using the Strapi context as parameters by default --- static/js/ReaderApp.jsx | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 7aadfded42..2e5b55db6e 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2190,28 +2190,22 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { // const { interruptingMessageModal } = useContext(StrapiDataContext); return ( - - -
- -
- {header} - {panels} - {sefariaModal} - {communityPagePreviewControls} - {beitMidrashPanel} - - {/* */} -
-
-
-
+ + +
+ +
+ {header} + {panels} + {sefariaModal} + {communityPagePreviewControls} + {beitMidrashPanel} + + {/* */} +
+
+
+
); } } From 408bd688911ae1bbc342d8e575d3d807f41fa3f9 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 13 Jul 2023 14:33:35 -0400 Subject: [PATCH 08/59] Simplifies determining who is returning and new visitor code --- static/js/Misc.jsx | 7 ++----- static/js/ReaderApp.jsx | 9 ++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 8573801002..5acca87a1b 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2191,13 +2191,10 @@ function OnInView({ children, onVisible }) { return
{children}
; } -// let isNewVisitor = JSON.parse(localStorage.getItem("isNewVisitor")); function isNewVisitor() { return ( - ("isNewVisitor" in sessionStorage && - JSON.parse(sessionStorage.getItem("isNewVisitor"))) || - ("isNewVisitor" in localStorage && - JSON.parse(localStorage.getItem("isNewVisitor"))) + "isNewVisitor" in sessionStorage && + JSON.parse(sessionStorage.getItem("isNewVisitor")) ); } diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 2e5b55db6e..6b1e4091c6 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -199,14 +199,13 @@ class ReaderApp extends Component { document.addEventListener('click', this.handleInAppClickWithModifiers, {capture: true}); // Save all initial panels to recently viewed this.state.panels.map(this.saveLastPlace); - // Initialize entries for first-time visitors to determine if they are new or returning - if (!("isNewVisitor" in localStorage) && !("isReturningVisitor" in localStorage)) { + // Initialize entries for first-time visitors to determine if they are new or returning presently or in the future + if (!("isNewVisitor" in sessionStorage) && !("isReturningVisitor" in localStorage)) { sessionStorage.setItem("isNewVisitor", "true"); - // Setting these at this time will make the current new visitor a returning one once their session is cleared - localStorage.setItem("isNewVisitor", "false"); + // Setting this at this time will make the current new visitor a returning one once their session is cleared localStorage.setItem("isReturningVisitor", "true"); } else if (Sefaria._uid) { - localStorage.setItem("isNewVisitor", "false"); + // A logged in user is automatically a returning visitor sessionStorage.setItem("isNewVisitor", "false"); localStorage.setItem("isReturningVisitor", "true"); } From 5784dcb1c757774357f12eb613c5fdfecb598668 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 14 Jul 2023 16:12:58 -0400 Subject: [PATCH 09/59] Adds initial support for banners that work with Strapi --- package-lock.json | 8 +-- package.json | 2 +- static/js/Misc.jsx | 139 +++++++++++++++++++++++++++++++++++++++ static/js/Promotions.jsx | 64 +++++++++--------- static/js/ReaderApp.jsx | 4 ++ static/js/context.js | 22 ++++++- 6 files changed, 202 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26f5d4edc1..0a2bd0d49b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "cheerio": "^0.22.0", "classnames": "^2.2.5", "cookie-parser": "^1.4.2", - "core-js": "^3.15.2", + "core-js": "^3.31.1", "css-modules-require-hook": "^4.2.3", "deep-merge": "^1.0.0", "diff-match-patch": "^1.0.0", @@ -4613,9 +4613,9 @@ } }, "node_modules/core-js": { - "version": "3.30.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.30.1.tgz", - "integrity": "sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ==", + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.1.tgz", + "integrity": "sha512-2sKLtfq1eFST7l7v62zaqXacPc7uG8ZAya8ogijLhTtaKNcpzpB4TMoTw2Si+8GYKRwFPMMtUT0263QFWFfqyQ==", "hasInstallScript": true, "funding": { "type": "opencollective", diff --git a/package.json b/package.json index ffd4e225b3..9097316243 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "cheerio": "^0.22.0", "classnames": "^2.2.5", "cookie-parser": "^1.4.2", - "core-js": "^3.15.2", + "core-js": "^3.31.1", "css-modules-require-hook": "^4.2.3", "deep-merge": "^1.0.0", "diff-match-patch": "^1.0.0", diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 5acca87a1b..4b24883758 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2191,6 +2191,8 @@ function OnInView({ children, onVisible }) { return
{children}
; } +// User could be new visitor when there isn't anything in sessionStorage either... +// Maybe don't check if it's in there or have extra conditional function isNewVisitor() { return ( "isNewVisitor" in sessionStorage && @@ -2245,6 +2247,7 @@ const InterruptingMessage = ({ // Use user context to determine whether this is valid for a user? // Maybe user context should be used to find if there's a compatible modal const shouldShow = () => { + console.log("checking whether to show modal or not"); if (!strapi.interruptingMessageModal) return false; if ( hasModalBeenInteractedWith( @@ -2424,6 +2427,141 @@ const InterruptingMessage = ({ }; InterruptingMessage.displayName = "InterruptingMessage"; +const Banner = ({ messageName, messageHTML, style, repetition, onClose }) => { + const [timesUp, setTimesUp] = useState(false); + const [hasInteractedWithBanner, setHasInteractedWithBanner] = useState(false); + const strapi = useContext(StrapiDataContext); + const showDelay = 5000; + + const markBannerAsHasBeenInteractedWith = (bannerName) => { + sessionStorage.setItem("banner_" + bannerName, "true"); + }; + + const hasBannerBeenInteractedWith = (bannerName) => { + return JSON.parse(sessionStorage.getItem("banner_" + bannerName)); + }; + + const trackBannerInteraction = (bannerName, eventDescription) => { + gtag("event", "banner_interacted_with_" + eventDescription, { + campaignID: bannerName, + adType: "banner", + }); + }; + + const trackBannerImpression = () => { + console.log("We've got visibility!"); + gtag("event", "banner_viewed", { + campaignID: strapi.banner.internalBannerName, + adType: "banner", + }); + }; + + const shouldShow = () => { + console.log("checking whether to show banner or not"); + if (!strapi.banner) return false; + if (hasBannerBeenInteractedWith(strapi.banner.internalBannerName)) + return false; + + let shouldShowBanner = false; + + let noUserKindIsSet = ![ + strapi.banner.showToReturningVisitors, + strapi.banner.showToNewVisitors, + strapi.banner.showToSustainers, + strapi.banner.showToNonSustainers, + ].some((p) => p); + if ( + Sefaria._uid && + ((Sefaria.is_sustainer && strapi.banner.showToSustainers) || + (!Sefaria.is_sustainer && strapi.banner.showToNonSustainers)) + ) + shouldShowBanner = true; + else if ( + (isReturningVisitor() && strapi.banner.showToReturningVisitors) || + (isNewVisitor() && strapi.banner.showToNewVisitors) + ) + shouldShowBanner = true; + else if (noUserKindIsSet) shouldShowBanner = true; + if (!shouldShowBanner) return false; + + const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; + return excludedPaths.indexOf(window.location.pathname) === -1; + }; + + const closeBanner = (eventDescription) => { + if (onClose) onClose(); + console.log(eventDescription); + markBannerAsHasBeenInteractedWith(strapi.banner.internalBannerName); + setHasInteractedWithBanner(true); + trackBannerInteraction( + strapi.banner.internalBannerName, + eventDescription + ); + }; + + useEffect(() => { + if (shouldShow()) { + console.log("reaching here..."); + + const timeoutId = setTimeout(() => { + // s2 is the div that contains the React root and needs to be manipulated by traditional DOM methods + if (document.getElementById("s2").classList.contains("headerOnly")) { + document.body.classList.add("hasBannerMessage"); + } + setTimesUp(true); + }, showDelay); + return () => clearTimeout(timeoutId); // clearTimeout on component unmount + } + }, [strapi.banner]); // execute useEffect when the modal changes + + if (!timesUp) return null; + console.log("data for the component"); + console.log(strapi.banner); + + if (!hasInteractedWithBanner) { + console.log("rendering component"); + return ( + +
+
+
+ {strapi.banner.bannerText} + + ספריית ספריא מנגישה יותר מ-300 מיליון מלים של טקסטים יהודיים + ברחבי העולם. לכבוד שבועות, אנא תמכו היום בספריה שמסייעת ללימוד + שלכם על-ידי קבלת מעמד של ידידי ספריא. + +
+ +
+
×
+
+ × +
+
+
+ ); + } else { + return null; + } +}; + +Banner.displayName = "Banner"; + + // InterruptingMessage.propTypes = { // messageName: PropTypes.string.isRequired, // messageHTML: PropTypes.string.isRequired, @@ -3272,6 +3410,7 @@ export { FollowButton, GlobalWarningMessage, InterruptingMessage, + Banner, InterfaceText, EnglishText, HebrewText, diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index d132e0cdce..c76a61aadb 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -18,38 +18,40 @@ const Promotions = ({ adType, rerender }) => { const sidebarAds = strapi.strapiData.sidebarAds.data; - sidebarAds.forEach((sidebarAd) => { - sidebarAd = sidebarAd.attributes; - console.log(JSON.stringify(sidebarAd, null, 2)); - let keywordTargetsArray = sidebarAd.keywords - .split(",") - .map((x) => x.trim().toLowerCase()); - Sefaria._inAppAds.push({ - campaignId: sidebarAd.internalCampaignId, - title: sidebarAd.Title, - bodyText: sidebarAd.bodyText, - buttonText: sidebarAd.buttonText, - buttonUrl: sidebarAd.buttonUrl, - buttonIcon: "", - buttonLocation: sidebarAd.buttonUrl, - adType: "sidebar", - hasBlueBackground: sidebarAd.hasBlueBackground, - repetition: 5, - buttonStyle: "", - trigger: { - showTo: sidebarAd.showTo, - interfaceLang: Sefaria.translateISOLanguageCode( - sidebarAd.locale - ).toLowerCase(), - dt_start: Date.parse(sidebarAd.startTime), - dt_end: Date.parse(sidebarAd.endTime), - keywordTargets: keywordTargetsArray, - excludeKeywordTargets: [], - }, - debug: sidebarAd.debug, + if (sidebarAds) { + sidebarAds.forEach((sidebarAd) => { + sidebarAd = sidebarAd.attributes; + console.log(JSON.stringify(sidebarAd, null, 2)); + let keywordTargetsArray = sidebarAd.keywords + .split(",") + .map((x) => x.trim().toLowerCase()); + Sefaria._inAppAds.push({ + campaignId: sidebarAd.internalCampaignId, + title: sidebarAd.Title, + bodyText: sidebarAd.bodyText, + buttonText: sidebarAd.buttonText, + buttonUrl: sidebarAd.buttonUrl, + buttonIcon: "", + buttonLocation: sidebarAd.buttonUrl, + adType: "sidebar", + hasBlueBackground: sidebarAd.hasBlueBackground, + repetition: 5, + buttonStyle: "", + trigger: { + showTo: sidebarAd.showTo, + interfaceLang: Sefaria.translateISOLanguageCode( + sidebarAd.locale + ).toLowerCase(), + dt_start: Date.parse(sidebarAd.startTime), + dt_end: Date.parse(sidebarAd.endTime), + keywordTargets: keywordTargetsArray, + excludeKeywordTargets: [], + }, + debug: sidebarAd.debug, + }); }); - }); - setInAppAds(Sefaria._inAppAds); + setInAppAds(Sefaria._inAppAds); + } } }, [strapi.dataFromStrapiHasBeenReceived]); // empty array happens when the page loads, equivalent of didcomponentmount diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 6b1e4091c6..dc2d7ecdf5 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -26,6 +26,7 @@ import { import { SignUpModal, InterruptingMessage, + Banner, CookiesNotification, CommunityPagePreviewControls } from './Misc'; @@ -2176,6 +2177,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { modalContentKind={this.state.modalContentKind} /> ); + const communityPagePreviewControls = this.props.communityPreview ? : null; @@ -2193,6 +2195,8 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
+ {/* */} +
{header} {panels} diff --git a/static/js/context.js b/static/js/context.js index 790cf305fa..62a9f45639 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -16,6 +16,7 @@ function StrapiDataProvider({ children }) { const [strapiData, setStrapiData] = useState(null); const [interruptingMessageModal, setInterruptingMessageModal] = useState(null); + const [banner, setBanner] = useState(null); useEffect(() => { const getStrapiData = async () => { try { @@ -32,7 +33,7 @@ function StrapiDataProvider({ children }) { let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); console.log(startDate); console.log(endDate); - const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalModalName\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; + const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalBannerName\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalModalName\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; console.log(query); const result = fetch("http://localhost:1337/graphql", { method: "POST", // *GET, POST, PUT, DELETE, etc. @@ -54,6 +55,9 @@ function StrapiDataProvider({ children }) { // e.g. there are modals with overlapping time frames let modals = result.data?.modals?.data; console.log(modals); + let banners = result.data?.banners?.data; + console.log(banners); + const currentDate = new Date(); if (modals?.length) { // if they end up being sorted, the first one will be the compatible one @@ -69,6 +73,21 @@ function StrapiDataProvider({ children }) { setInterruptingMessageModal(modal.attributes); } } + + if (banners?.length) { + let b = banners.find( + (b) => + currentDate >= new Date(b.attributes.bannerStartDate) && + currentDate <= new Date(b.attributes.bannerEndDate) + ); + console.log("found acceptable banner:"); + console.log(b); + if (b) { + console.log("setting the banner"); + setBanner(b.attributes); + console.log(b.attributes); + } + } }); } catch (error) { console.error("Failed to get strapi data", error); @@ -83,6 +102,7 @@ function StrapiDataProvider({ children }) { dataFromStrapiHasBeenReceived, strapiData, interruptingMessageModal, + banner, }} > {children} From 64cc7ddf4bc70ee607fccd0c602ad4c76cf6436a Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 14 Jul 2023 16:19:14 -0400 Subject: [PATCH 10/59] Adds button text and URL for Strapi banners --- static/js/Misc.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 4b24883758..31ba607c6a 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2535,9 +2535,9 @@ const Banner = ({ messageName, messageHTML, style, repetition, onClose }) => {
- Sustain Sefaria + {strapi.banner.buttonText} Date: Wed, 19 Jul 2023 17:33:23 -0400 Subject: [PATCH 11/59] Renames state variable interruptingMessageModal to simply be modal to make a possible refactor easier and FINALLY gets a working literal GraphQL query as a multiline template string --- static/js/context.js | 98 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 6 deletions(-) diff --git a/static/js/context.js b/static/js/context.js index 62a9f45639..955d77d7d8 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -14,7 +14,7 @@ function StrapiDataProvider({ children }) { const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = useState(false); const [strapiData, setStrapiData] = useState(null); - const [interruptingMessageModal, setInterruptingMessageModal] = + const [modal, setModal] = useState(null); const [banner, setBanner] = useState(null); useEffect(() => { @@ -33,8 +33,94 @@ function StrapiDataProvider({ children }) { let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); console.log(startDate); console.log(endDate); - const query = `{"query":"# Write your query or mutation here\\nquery {\\n banners(filters: {\\n bannerStartDate: { gte: \\"${startDate}\\" }\\n and: [{ bannerEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalBannerName\\n bannerEndDate\\n bannerStartDate\\n bannerText\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n modals(filters: {\\n modalStartDate: { gte: \\"${startDate}\\" }\\n and: [{ modalEndDate: { lte: \\"${endDate}\\" } }]\\n } ) {\\n data {\\n id\\n attributes {\\n internalModalName\\n buttonText\\n buttonURL\\n createdAt\\n locale\\n modalEndDate\\n modalStartDate\\n modalText\\n publishedAt\\n shouldDeployOnMobile\\n showToNewVisitors\\n showToNonSustainers\\n showToReturningVisitors\\n showToSustainers\\n updatedAt\\n }\\n }\\n }\\n sidebarAds(filters: {\\n startTime: { gte: \\"${startDate}\\" }\\n and: [{ endTime: { lte: \\"${endDate}\\" } }]\\n } ){\\n data {\\n id\\n attributes {\\n ButtonAboveOrBelow\\n Title\\n bodyText\\n buttonText\\n buttonUrl\\n createdAt\\n debug\\n endTime\\n hasBlueBackground\\n internalCampaignId\\n keywords\\n locale\\n publishedAt\\n showTo\\n startTime\\n updatedAt\\n }\\n }\\n }\\n}"}`; - console.log(query); + const query = + ` + query { + banners( + filters: { + bannerStartDate: { gte: \"${startDate}\" } + and: [{ bannerEndDate: { lte: \"${endDate}\" } }] + } + ) { + data { + id + attributes { + internalBannerName + bannerEndDate + bannerStartDate + bannerText + buttonText + buttonURL + createdAt + locale + publishedAt + shouldDeployOnMobile + showToNewVisitors + showToNonSustainers + showToReturningVisitors + showToSustainers + updatedAt + } + } + } + modals( + filters: { + modalStartDate: { gte: \"${startDate}\" } + and: [{ modalEndDate: { lte: \"${endDate}\" } }] + } + ) { + data { + id + attributes { + internalModalName + buttonText + buttonURL + createdAt + locale + modalEndDate + modalStartDate + modalText + publishedAt + shouldDeployOnMobile + showToNewVisitors + showToNonSustainers + showToReturningVisitors + showToSustainers + updatedAt + } + } + } + sidebarAds( + filters: { + startTime: { gte: \"${startDate}\" } + and: [{ endTime: { lte: \"${endDate}\" } }] + } + ) { + data { + id + attributes { + ButtonAboveOrBelow + Title + bodyText + buttonText + buttonUrl + createdAt + debug + endTime + hasBlueBackground + internalCampaignId + keywords + locale + publishedAt + showTo + startTime + updatedAt + } + } + } + } + `; + const result = fetch("http://localhost:1337/graphql", { method: "POST", // *GET, POST, PUT, DELETE, etc. mode: "cors", // no-cors, *cors, same-origin @@ -45,7 +131,7 @@ function StrapiDataProvider({ children }) { }, redirect: "follow", // manual, *follow, error referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: query, + body: JSON.stringify({query}), }) .then((response) => response.json()) .then((result) => { @@ -70,7 +156,7 @@ function StrapiDataProvider({ children }) { console.log(modal); if (modal) { console.log("setting the modal"); - setInterruptingMessageModal(modal.attributes); + setModal(modal.attributes); } } @@ -101,7 +187,7 @@ function StrapiDataProvider({ children }) { value={{ dataFromStrapiHasBeenReceived, strapiData, - interruptingMessageModal, + modal, banner, }} > From c44ba40d91b8c3797017ef9fea9d061967cdda20 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 19 Jul 2023 17:50:39 -0400 Subject: [PATCH 12/59] Adds localization support for Hebrew to the GraphQL query for banners and modals --- static/js/context.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/static/js/context.js b/static/js/context.js index 955d77d7d8..bc8eed86b8 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -53,6 +53,16 @@ function StrapiDataProvider({ children }) { buttonURL createdAt locale + localizations { + data { + attributes { + locale + buttonText + buttonURL + bannerText + } + } + } publishedAt shouldDeployOnMobile showToNewVisitors @@ -77,6 +87,16 @@ function StrapiDataProvider({ children }) { buttonURL createdAt locale + localizations { + data { + attributes { + locale + buttonText + buttonURL + modalText + } + } + } modalEndDate modalStartDate modalText From 3c3dc25a816dd3192021f8b29cf2987d656a0593 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 19 Jul 2023 17:53:14 -0400 Subject: [PATCH 13/59] Changes the name of the strapi modal object to be just modal for easier future refactoring. Also adds commented out start of a refactored component for both modals and banners --- static/js/Misc.jsx | 452 ++++++++++++++++++++++++++++----------- static/js/Promotions.jsx | 2 +- 2 files changed, 324 insertions(+), 130 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 31ba607c6a..bab90df5e7 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2057,111 +2057,6 @@ SignUpModal.propTypes = { modalContent: PropTypes.object.isRequired, }; - -// class InterruptingMessage extends Component { -// constructor(props) { -// super(props); -// this.displayName = 'InterruptingMessage'; -// this.state = { -// timesUp: false, -// animationStarted: false -// }; -// this.settings = { -// "modal": { -// "trackingName": "Interrupting Message", -// "showDelay": 10000, -// }, -// "banner": { -// "trackingName": "Banner Message", -// "showDelay": 1, -// } -// }[this.props.style]; -// } -// componentDidMount() { -// if (this.shouldShow()) { -// this.delayedShow(); -// } -// } -// shouldShow() { -// const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; -// return excludedPaths.indexOf(window.location.pathname) === -1; -// } -// delayedShow() { -// setTimeout(function() { -// this.setState({timesUp: true}); -// $("#interruptingMessage .button").click(this.close); -// $("#interruptingMessage .trackedAction").click(this.trackAction); -// this.showAorB(); -// this.animateOpen(); -// }.bind(this), this.settings.showDelay); -// } -// animateOpen() { -// setTimeout(function() { -// if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").addClass("hasBannerMessage"); } -// this.setState({animationStarted: true}); -// this.trackOpen(); -// }.bind(this), 50); -// } -// showAorB() { -// // Allow random A/B testing if items are tagged ".optionA", ".optionB" -// const $message = $(ReactDOM.findDOMNode(this)); -// if ($message.find(".optionA").length) { -// console.log("rand show") -// Math.random() > 0.5 ? $(".optionA").show() : $(".optionB").show(); -// } -// } -// close() { -// this.markAsRead(); -// this.props.onClose(); -// // if (this.props.style === "banner" && $("#s2").hasClass("headerOnly")) { $("body").removeClass("hasBannerMessage"); } -// } -// trackOpen() { -// Sefaria.track.event(this.settings.trackingName, "open", this.props.messageName, { nonInteraction: true }); -// } -// trackAction() { -// Sefaria.track.event(this.settings.trackingName, "action", this.props.messageName, { nonInteraction: true }); -// } -// markAsRead() { -// Sefaria._api("/api/interrupting-messages/read/" + this.props.messageName, function (data) {}); -// var cookieName = this.props.messageName + "_" + this.props.repetition; -// $.cookie(cookieName, true, { path: "/", expires: 14 }); -// Sefaria.track.event(this.settings.trackingName, "read", this.props.messageName, { nonInteraction: true }); -// Sefaria.interruptingMessage = null; -// } -// render() { -// if (!this.state.timesUp) { return null; } - -// if (this.props.style === "banner") { -// return
-//
-//
×
-//
; - -// } else if (this.props.style === "modal") { -// return
-//
-//
-//
-//
-//
×
-//
-// {/*
*/} -//
-//
-//
-//
; -// } -// return null; -// } -// } -// InterruptingMessage.propTypes = { -// messageName: PropTypes.string.isRequired, -// messageHTML: PropTypes.string.isRequired, -// style: PropTypes.string.isRequired, -// repetition: PropTypes.number.isRequired, // manual toggle to refresh an existing message -// onClose: PropTypes.func.isRequired -// }; - // Write comments explaining how this works function OnInView({ children, onVisible }) { const elementRef = useRef(); @@ -2209,10 +2104,6 @@ function isReturningVisitor() { } const InterruptingMessage = ({ - messageName, - messageHTML, - style, - repetition, onClose, }) => { const [timesUp, setTimesUp] = useState(false); @@ -2238,7 +2129,7 @@ const InterruptingMessage = ({ const trackModalImpression = () => { console.log("We've got visibility!"); gtag("event", "modal_viewed", { - campaignID: strapi.interruptingMessageModal.internalModalName, + campaignID: strapi.modal.internalModalName, adType: "modal", }); }; @@ -2248,34 +2139,35 @@ const InterruptingMessage = ({ // Maybe user context should be used to find if there's a compatible modal const shouldShow = () => { console.log("checking whether to show modal or not"); - if (!strapi.interruptingMessageModal) return false; + if (!strapi.modal) return false; if ( hasModalBeenInteractedWith( - strapi.interruptingMessageModal.internalModalName + strapi.modal.internalModalName ) ) return false; + console.log('lets check a modal'); let shouldShowModal = false; let noUserKindIsSet = ![ - strapi.interruptingMessageModal.showToReturningVisitors, - strapi.interruptingMessageModal.showToNewVisitors, - strapi.interruptingMessageModal.showToSustainers, - strapi.interruptingMessageModal.showToNonSustainers, + strapi.modal.showToReturningVisitors, + strapi.modal.showToNewVisitors, + strapi.modal.showToSustainers, + strapi.modal.showToNonSustainers, ].some((p) => p); if ( Sefaria._uid && ((Sefaria.is_sustainer && - strapi.interruptingMessageModal.showToSustainers) || + strapi.modal.showToSustainers) || (!Sefaria.is_sustainer && - strapi.interruptingMessageModal.showToNonSustainers)) + strapi.modal.showToNonSustainers)) ) shouldShowModal = true; else if ( (isReturningVisitor() && - strapi.interruptingMessageModal.showToReturningVisitors) || - (isNewVisitor() && strapi.interruptingMessageModal.showToNewVisitors) + strapi.modal.showToReturningVisitors) || + (isNewVisitor() && strapi.modal.showToNewVisitors) ) shouldShowModal = true; else if (noUserKindIsSet) shouldShowModal = true; @@ -2289,11 +2181,11 @@ const InterruptingMessage = ({ if (onClose) onClose(); console.log(eventDescription); markModalAsHasBeenInteractedWith( - strapi.interruptingMessageModal.internalModalName + strapi.modal.internalModalName ); setHasInteractedWithModal(true); trackModalInteraction( - strapi.interruptingMessageModal.internalModalName, + strapi.modal.internalModalName, eventDescription ); }; @@ -2305,11 +2197,11 @@ const InterruptingMessage = ({ }, showDelay); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } - }, [strapi.interruptingMessageModal]); // execute useEffect when the modal changes + }, [strapi.modal]); // execute useEffect when the modal changes if (!timesUp) return null; console.log("data for the component"); - console.log(strapi.interruptingMessageModal); + console.log(strapi.modal); if (!hasInteractedWithModal) { console.log("rendering component"); @@ -2328,7 +2220,7 @@ const InterruptingMessage = ({ > ×
- {/*
*/} + {/*
*/}
+// +//
+//
+//
+//
+//
+// +// ); +// } else { +// return null; +// } +// } +// }; + + + +// const InterruptingComponent = ({ componentKind, componentName, showDelay, beforeShowingUp, onClose }) => { +// const [timesUp, setTimesUp] = useState(false); +// const [hasInteractedWithComponent, setHasInteractedWithComponent] = useState(false); +// const strapi = useContext(StrapiDataContext); + +// const markComponentAsHasBeenInteractedWith = (componentName) => { +// sessionStorage.setItem(componentKind + "_" + componentName, "true"); +// }; + +// const hasComponentBeenInteractedWith = (bannerName) => { +// return JSON.parse(sessionStorage.getItem("banner_" + bannerName)); +// }; + +// const trackBannerInteraction = (bannerName, eventDescription) => { +// gtag("event", "banner_interacted_with_" + eventDescription, { +// campaignID: bannerName, +// adType: "banner", +// }); +// }; + +// const trackBannerImpression = () => { +// console.log("We've got visibility!"); +// gtag("event", "banner_viewed", { +// campaignID: strapi.banner.internalBannerName, +// adType: "banner", +// }); +// }; + +// const shouldShow = () => { +// console.log("checking whether to show banner or not"); +// if (!strapi.banner) return false; +// if (hasBannerBeenInteractedWith(strapi.banner.internalBannerName)) +// return false; + +// let shouldShowBanner = false; + +// let noUserKindIsSet = ![ +// strapi.banner.showToReturningVisitors, +// strapi.banner.showToNewVisitors, +// strapi.banner.showToSustainers, +// strapi.banner.showToNonSustainers, +// ].some((p) => p); +// if ( +// Sefaria._uid && +// ((Sefaria.is_sustainer && strapi.banner.showToSustainers) || +// (!Sefaria.is_sustainer && strapi.banner.showToNonSustainers)) +// ) +// shouldShowBanner = true; +// else if ( +// (isReturningVisitor() && strapi.banner.showToReturningVisitors) || +// (isNewVisitor() && strapi.banner.showToNewVisitors) +// ) +// shouldShowBanner = true; +// else if (noUserKindIsSet) shouldShowBanner = true; +// if (!shouldShowBanner) return false; + +// const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; +// return excludedPaths.indexOf(window.location.pathname) === -1; +// }; + +// const closeBanner = (eventDescription) => { +// if (onClose) onClose(); +// console.log(eventDescription); +// markBannerAsHasBeenInteractedWith(strapi.banner.internalBannerName); +// setHasInteractedWithBanner(true); +// trackBannerInteraction( +// strapi.banner.internalBannerName, +// eventDescription +// ); +// }; + +// useEffect(() => { +// if (shouldShow()) { +// console.log("reaching here..."); + +// const timeoutId = setTimeout(() => { +// // s2 is the div that contains the React root and needs to be manipulated by traditional DOM methods +// if (document.getElementById("s2").classList.contains("headerOnly")) { +// document.body.classList.add("hasBannerMessage"); +// } +// setTimesUp(true); +// }, showDelay); +// return () => clearTimeout(timeoutId); // clearTimeout on component unmount +// } +// }, [strapi.banner]); // execute useEffect when the modal changes + +// if (!timesUp) return null; +// console.log("data for the component"); +// console.log(strapi.banner); + +// if (!hasInteractedWithBanner) { +// console.log("rendering component"); +// return ( +// +//
+//
+//
+// {strapi.banner.bannerText} +// +// ספריית ספריא מנגישה יותר מ-300 מיליון מלים של טקסטים יהודיים +// ברחבי העולם. לכבוד שבועות, אנא תמכו היום בספריה שמסייעת ללימוד +// שלכם על-ידי קבלת מעמד של ידידי ספריא. +// +//
+// +//
+//
×
+//
+// × +//
+//
+//
+// ); +// } else { +// return null; +// } +// }; + // InterruptingMessage.propTypes = { // messageName: PropTypes.string.isRequired, diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index c76a61aadb..d1f39d97dd 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -16,7 +16,7 @@ const Promotions = ({ adType, rerender }) => { console.log("we got some data"); console.log(JSON.stringify(strapi.strapiData, null, 2)); - const sidebarAds = strapi.strapiData.sidebarAds.data; + const sidebarAds = strapi.strapiData?.sidebarAds?.data; if (sidebarAds) { sidebarAds.forEach((sidebarAd) => { From fcb5abd4b6418a51b44128abe17172a11518ed4a Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 20 Jul 2023 13:47:41 -0400 Subject: [PATCH 14/59] Adds Hebrew/Markdown support for banners and modals --- static/js/Misc.jsx | 41 +++++++++++++++++++++++----------------- static/js/Promotions.jsx | 1 + static/js/context.js | 39 +++++++++++++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 22 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index bab90df5e7..432f5d7796 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -29,7 +29,7 @@ import TrackG4 from "./sefaria/trackG4"; * * lorem ipsum * lorem ipsum - * + * * ``` * @param children * @returns {JSX.Element} @@ -2287,21 +2287,31 @@ const InterruptingMessage = ({

- - {strapi.modal.modalText} - +

@@ -2351,6 +2361,7 @@ const Banner = ({ onClose }) => { const shouldShow = () => { console.log("checking whether to show banner or not"); if (!strapi.banner) return false; + if (Sefaria.interfaceLang === 'hebrew' && !strapi.banner.locales.includes('he')) return false; if (hasBannerBeenInteractedWith(strapi.banner.internalBannerName)) return false; @@ -2411,31 +2422,27 @@ const Banner = ({ onClose }) => { console.log(strapi.banner); if (!hasInteractedWithBanner) { - console.log("rendering component"); + console.log("rendering banner"); + console.log(strapi.banner.bannerText); return (
- {strapi.banner.bannerText} - - ספריית ספריא מנגישה יותר מ-300 מיליון מלים של טקסטים יהודיים - ברחבי העולם. לכבוד שבועות, אנא תמכו היום בספריה שמסייעת ללימוד - שלכם על-ידי קבלת מעמד של ידידי ספריא. - +
diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index d1f39d97dd..794b8b85a2 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -224,6 +224,7 @@ const Promotions = ({ adType, rerender }) => { if (!matchingAd) { return null; } + // TODO: change this to use new InterruptingMessage const bannerHtml = createBannerHtml(matchingAd); return ( { + modal.attributes[attribute] = { en: modal.attributes[attribute], he: hebrew_attributes[attribute] } + }); + modal.attributes.locales = ['en', 'he']; + } + else { + ['modalText', 'buttonText', 'buttonURL'].forEach((attribute) => { + modal.attributes[attribute] = { en: modal.attributes[attribute], he: null }; + }); + modal.attributes.locales = ['en']; + } setModal(modal.attributes); } } if (banners?.length) { - let b = banners.find( + let banner = banners.find( (b) => currentDate >= new Date(b.attributes.bannerStartDate) && currentDate <= new Date(b.attributes.bannerEndDate) ); + console.log("found acceptable banner:"); - console.log(b); - if (b) { + console.log(banner); + if (banner) { console.log("setting the banner"); - setBanner(b.attributes); - console.log(b.attributes); + if (banner.attributes.localizations?.data?.length) { + let localization_attributes = banner.attributes.localizations.data[0].attributes; + let {locale, ...hebrew_attributes} = localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + banner.attributes[attribute] = { en: banner.attributes[attribute], he: hebrew_attributes[attribute] } + }); + banner.attributes.locales = ['en', 'he']; + } else { + // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? + ['bannerText', 'buttonText', 'buttonURL'].forEach((attribute) => { + banner.attributes[attribute] = { en: banner.attributes[attribute], he: null }; + }); + banner.attributes.locales = ['en']; + } + setBanner(banner.attributes); + console.log(banner.attributes); } } }); From 7c7ff4e4d11a5d68aff336f7a8334f707281660b Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 20 Jul 2023 13:50:03 -0400 Subject: [PATCH 15/59] Fixes unintentional typo --- static/js/Misc.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 432f5d7796..a8faece559 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -29,7 +29,7 @@ import TrackG4 from "./sefaria/trackG4"; * * lorem ipsum * lorem ipsum - * + * * ``` * @param children * @returns {JSX.Element} From 10c4773baa58da6609d89a1498b3e424674fbf2f Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 24 Jul 2023 01:47:18 -0400 Subject: [PATCH 16/59] Reformats code --- static/js/context.js | 266 ++++++++++++++++++++++--------------------- 1 file changed, 136 insertions(+), 130 deletions(-) diff --git a/static/js/context.js b/static/js/context.js index 6be5ee62f2..0cb6f9e2f0 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -1,6 +1,7 @@ -import React, { useContext, useEffect, useState } from "react"; +import React, {useContext, useEffect, useState} from "react"; + const ContentLanguageContext = React.createContext({ - language: "english", + language: "english", }); ContentLanguageContext.displayName = "ContentLanguageContext"; //This lets us see this name in the devtools @@ -10,31 +11,31 @@ AdContext.displayName = "AdContext"; const StrapiDataContext = React.createContext({}); StrapiDataContext.displayName = "StrapiDataContext"; -function StrapiDataProvider({ children }) { - const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = - useState(false); - const [strapiData, setStrapiData] = useState(null); - const [modal, setModal] = - useState(null); - const [banner, setBanner] = useState(null); - useEffect(() => { - const getStrapiData = async () => { - try { - let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; - let getJSONDateStringInLocalTimeZone = (date) => { - let parts = getDateWithoutTime(date).split("-"); - return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); - }; - let currentDate = new Date(); - let oneWeekFromNow = new Date(); - oneWeekFromNow.setDate(currentDate.getDate() + 7); - currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC - let startDate = getJSONDateStringInLocalTimeZone(currentDate); - let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); - console.log(startDate); - console.log(endDate); - const query = - ` +function StrapiDataProvider({children}) { + const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = + useState(false); + const [strapiData, setStrapiData] = useState(null); + const [modal, setModal] = + useState(null); + const [banner, setBanner] = useState(null); + useEffect(() => { + const getStrapiData = async () => { + try { + let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; + let getJSONDateStringInLocalTimeZone = (date) => { + let parts = getDateWithoutTime(date).split("-"); + return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); + }; + let currentDate = new Date(); + let oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(currentDate.getDate() + 7); + currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC + let startDate = getJSONDateStringInLocalTimeZone(currentDate); + let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); + console.log(startDate); + console.log(endDate); + const query = + ` query { banners( filters: { @@ -140,109 +141,114 @@ function StrapiDataProvider({ children }) { } } `; - - const result = fetch("http://localhost:1337/graphql", { - method: "POST", // *GET, POST, PUT, DELETE, etc. - mode: "cors", // no-cors, *cors, same-origin - cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - headers: { - "Content-Type": "application/json", - }, - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: JSON.stringify({query}), - }) - .then((response) => response.json()) - .then((result) => { - setStrapiData(result.data); - setDataFromStrapiHasBeenReceived(true); - // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists - // e.g. there are modals with overlapping time frames - let modals = result.data?.modals?.data; - console.log(modals); - let banners = result.data?.banners?.data; - console.log(banners); - const currentDate = new Date(); - if (modals?.length) { - // if they end up being sorted, the first one will be the compatible one - let modal = modals.find( - (modal) => - currentDate >= new Date(modal.attributes.modalStartDate) && - currentDate <= new Date(modal.attributes.modalEndDate) - ); - console.log("found acceptable modal:"); - console.log(modal); - if (modal) { - console.log("setting the modal"); - if (modal.attributes.localizations?.data?.length) { - let localization_attributes = modal.attributes.localizations.data[0].attributes; - let {locale, ...hebrew_attributes} = localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { - modal.attributes[attribute] = { en: modal.attributes[attribute], he: hebrew_attributes[attribute] } - }); - modal.attributes.locales = ['en', 'he']; - } - else { - ['modalText', 'buttonText', 'buttonURL'].forEach((attribute) => { - modal.attributes[attribute] = { en: modal.attributes[attribute], he: null }; - }); - modal.attributes.locales = ['en']; - } - setModal(modal.attributes); - } - } + const result = fetch("http://localhost:1337/graphql", { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", // include, *same-origin, omit + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", // manual, *follow, error + referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url + body: JSON.stringify({query}), + }) + .then((response) => response.json()) + .then((result) => { + setStrapiData(result.data); + setDataFromStrapiHasBeenReceived(true); + // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists + // e.g. there are modals with overlapping time frames + let modals = result.data?.modals?.data; + console.log(modals); + let banners = result.data?.banners?.data; + console.log(banners); - if (banners?.length) { - let banner = banners.find( - (b) => - currentDate >= new Date(b.attributes.bannerStartDate) && - currentDate <= new Date(b.attributes.bannerEndDate) - ); + const currentDate = new Date(); + if (modals?.length) { + // if they end up being sorted, the first one will be the compatible one + let modal = modals.find( + (modal) => + currentDate >= new Date(modal.attributes.modalStartDate) && + currentDate <= new Date(modal.attributes.modalEndDate) + ); + console.log("found acceptable modal:"); + console.log(modal); + if (modal) { + console.log("setting the modal"); + if (modal.attributes.localizations?.data?.length) { + let localization_attributes = modal.attributes.localizations.data[0].attributes; + let {locale, ...hebrew_attributes} = localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: hebrew_attributes[attribute] + } + }); + modal.attributes.locales = ['en', 'he']; + } else { + ['modalText', 'buttonText', 'buttonURL'].forEach((attribute) => { + modal.attributes[attribute] = {en: modal.attributes[attribute], he: null}; + }); + modal.attributes.locales = ['en']; + } + setModal(modal.attributes); + } + } - console.log("found acceptable banner:"); - console.log(banner); - if (banner) { - console.log("setting the banner"); - if (banner.attributes.localizations?.data?.length) { - let localization_attributes = banner.attributes.localizations.data[0].attributes; - let {locale, ...hebrew_attributes} = localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { - banner.attributes[attribute] = { en: banner.attributes[attribute], he: hebrew_attributes[attribute] } - }); - banner.attributes.locales = ['en', 'he']; - } else { - // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? - ['bannerText', 'buttonText', 'buttonURL'].forEach((attribute) => { - banner.attributes[attribute] = { en: banner.attributes[attribute], he: null }; - }); - banner.attributes.locales = ['en']; - } - setBanner(banner.attributes); - console.log(banner.attributes); - } + if (banners?.length) { + let banner = banners.find( + (b) => + currentDate >= new Date(b.attributes.bannerStartDate) && + currentDate <= new Date(b.attributes.bannerEndDate) + ); + + console.log("found acceptable banner:"); + console.log(banner); + if (banner) { + console.log("setting the banner"); + if (banner.attributes.localizations?.data?.length) { + let localization_attributes = banner.attributes.localizations.data[0].attributes; + let {locale, ...hebrew_attributes} = localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + banner.attributes[attribute] = { + en: banner.attributes[attribute], + he: hebrew_attributes[attribute] + } + }); + banner.attributes.locales = ['en', 'he']; + } else { + // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? + ['bannerText', 'buttonText', 'buttonURL'].forEach((attribute) => { + banner.attributes[attribute] = {en: banner.attributes[attribute], he: null}; + }); + banner.attributes.locales = ['en']; + } + setBanner(banner.attributes); + console.log(banner.attributes); + } + } + }); + } catch (error) { + console.error("Failed to get strapi data", error); } - }); - } catch (error) { - console.error("Failed to get strapi data", error); - } - }; - getStrapiData(); - }, []); + }; + getStrapiData(); + }, []); - return ( - - {children} - - ); + return ( + + {children} + + ); } // function ExampleComponent() { @@ -259,9 +265,9 @@ function StrapiDataProvider({ children }) { // } export { - ContentLanguageContext, - AdContext, - StrapiDataProvider, - // ExampleComponent, - StrapiDataContext, + ContentLanguageContext, + AdContext, + StrapiDataProvider, + // ExampleComponent, + StrapiDataContext, }; From ae5645056e8fab157875fc8684d83f087636a8f6 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 25 Jul 2023 20:59:51 -0400 Subject: [PATCH 17/59] Adds environment variables for pointing to a Strapi instance --- sefaria/system/context_processors.py | 2 ++ static/js/context.js | 5 +++-- templates/base.html | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/sefaria/system/context_processors.py b/sefaria/system/context_processors.py index d482e49bf9..cc7a71afa6 100644 --- a/sefaria/system/context_processors.py +++ b/sefaria/system/context_processors.py @@ -68,6 +68,8 @@ def global_settings(request): return { "SEARCH_INDEX_NAME_TEXT": SEARCH_INDEX_NAME_TEXT, "SEARCH_INDEX_NAME_SHEET":SEARCH_INDEX_NAME_SHEET, + "STRAPI_LOCATION": STRAPI_LOCATION, + "STRAPI_PORT": STRAPI_PORT, "GOOGLE_TAG_MANAGER_CODE":GOOGLE_TAG_MANAGER_CODE, "GOOGLE_GTAG": GOOGLE_GTAG, "HOTJAR_ID": HOTJAR_ID, diff --git a/static/js/context.js b/static/js/context.js index 0cb6f9e2f0..c6de85fee1 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -19,6 +19,7 @@ function StrapiDataProvider({children}) { useState(null); const [banner, setBanner] = useState(null); useEffect(() => { + if (STRAPI_INSTANCE) { const getStrapiData = async () => { try { let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; @@ -141,8 +142,7 @@ function StrapiDataProvider({children}) { } } `; - - const result = fetch("http://localhost:1337/graphql", { + const result = fetch(STRAPI_INSTANCE + "/graphql", { method: "POST", // *GET, POST, PUT, DELETE, etc. mode: "cors", // no-cors, *cors, same-origin cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached @@ -235,6 +235,7 @@ function StrapiDataProvider({children}) { } }; getStrapiData(); + } }, []); return ( diff --git a/templates/base.html b/templates/base.html index 913d708e7c..a8f6417b17 100644 --- a/templates/base.html +++ b/templates/base.html @@ -237,6 +237,10 @@ static_url: {{ STATIC_PREFIX }}, }; + {% if STRAPI_LOCATION and STRAPI_PORT %} + var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; + {% endif %} + {% endautoescape %} From 64dfbc61bef1420929de670fae5f1388b5c4e687 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 25 Jul 2023 21:02:39 -0400 Subject: [PATCH 18/59] Adds environment variables to example local settings with an explanation --- sefaria/local_settings_example.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sefaria/local_settings_example.py b/sefaria/local_settings_example.py index f6b1fbf201..3037ee7981 100644 --- a/sefaria/local_settings_example.py +++ b/sefaria/local_settings_example.py @@ -132,6 +132,11 @@ } """ +# Location of Strapi CMS instance +# For local development, Strapi is located at http://localhost:1337 by default +STRAPI_LOCATION = None +STRAPI_PORT = None + MANAGERS = ADMINS From 861faf4744d62d4d64316a257fff7bed76fa77e4 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 26 Jul 2023 11:25:26 -0400 Subject: [PATCH 19/59] Fixes bug for when there is no hebrew translation for a modal --- static/js/Misc.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index a8faece559..f3cf7bf585 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2140,6 +2140,7 @@ const InterruptingMessage = ({ const shouldShow = () => { console.log("checking whether to show modal or not"); if (!strapi.modal) return false; + if (Sefaria.interfaceLang === 'hebrew' && !strapi.modal.locales.includes('he')) return false; if ( hasModalBeenInteractedWith( strapi.modal.internalModalName From 81a6ec577fdc9e1e43d204ae5f2ce0db9b2e30f7 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 26 Jul 2023 15:19:22 -0400 Subject: [PATCH 20/59] Removes commented out code and moves styles from modal component to css file --- static/css/s2.css | 80 ++++++++++ static/js/Misc.jsx | 366 --------------------------------------------- 2 files changed, 80 insertions(+), 366 deletions(-) diff --git a/static/css/s2.css b/static/css/s2.css index 1f147ac52c..d7927bde39 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -1217,6 +1217,86 @@ div.interfaceLinks-row a { .interface-hebrew #interruptingMessage h1{ font-style: normal; } + +/* Styles used from previously existing modals */ + +#highHolidayDonation { + width: 410px; + max-height: 100%; + max-width: 100%; +} + +.interface-english #highHolidayDonation { + text-align: left; +} + +.interface-hebrew #highHolidayDonation { + text-align: right; + direction: rtl; +} + +#highHolidayDonation p { + color: #555; +} + +.interface-hebrew p.int-en { + display: none; +} + +#highHolidayDonation p .int-en { + font-family: "adobe-garamond-pro", Georgia, serif; +} + +#highHolidayDonation p .int-he { + font-family: "adobe-garamond-pro", Georgia, serif; + /* font-family: "Heebo", sans-serif; */ +} + +#highHolidayDonation p.sub { + color: #999; + font-size: 12px; + font-family: "Roboto", "Helvetica Neue", Helvetica, sans-serif; +} + +#highHolidayDonation p { + margin-top: 0; +} + +#highHolidayDonation .button { + margin-bottom: 20px; +} + +#highHolidayDonation img { + max-width: 100%; +} + +#highHolidayDonation .buttons { + text-align: right; +} + +.leader { + font-weight: bold; +} + +.center { + text-align: center; +} + +#email-input-wrapper { + display: flex; + align-items: flex-start; + flex-direction: column; +} + +.newsletterInput#email-input { + width: 300px; + padding: 10px; + margin-bottom: 20px; + border-radius: 7px; + border: 1px solid #EEE; + color: #333; +} + .header .my-profile img { height: 24px; width: 24px; diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index f3cf7bf585..ec2c263a99 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2221,71 +2221,7 @@ const InterruptingMessage = ({ > ×
- {/*
*/}
-

@@ -2461,308 +2397,6 @@ const Banner = ({ onClose }) => { Banner.displayName = "Banner"; -// const useInterruptingComponent = (componentKind, showDelay, onClose, nameId) => { -// const [timesUp, setTimesUp] = useState(false); -// const [hasComponentBeenInteractedWith, setComponentHasBeenInteractedWith] = useState(false); -// const strapi = useContext(StrapiDataContext); - -// const markComponentAsHasBeenInteractedWith = (name) => { -// sessionStorage.setItem(componentKind + "_" + name, "true"); -// } - -// const hasBeenInteractedWith = (name) => { -// return JSON.parse(sessionStorage.getItem(componentKind + "_" + name)); -// } - -// const trackInteraction = (name, eventDescription) => { -// gtag("event", componentKind + "_interacted_with_" + eventDescription, { -// campaignID: name, -// adType: componentKind, -// }); -// } - -// const trackImpression = (name) => { -// console.log("We've got visibility!"); -// gtag("event", componentKind + "_viewed", { -// campaignID: name, -// adType: componentKind, -// }); -// } - -// const closeComponent = (eventDescription) => { -// if (onClose) onClose(); -// markComponentAsHasBeenInteractedWith(strapiData[nameId]); -// setComponentHasBeenInteractedWith(true); -// trackInteraction(strapiData[nameId], eventDescription); -// } - -// useEffect(() => { -// if (shouldShow()) { -// const timeoutId = setTimeout(() => { -// setTimesUp(true); -// }, showDelay); -// return () => clearTimeout(timeoutId); -// } -// }, [strapi[componentKind]]); - -// return {timesUp, hasComponentBeenInteractedWith, closeComponent, trackImpression, strapiData: strapi[componentKind]}; -// }; - -// const InterruptingMessage = ({ onClose }) => { -// const { timesUp, hasComponentBeenInteractedWith, closeComponent, trackImpression, strapiData } = useInterruptingComponent('modal', 5000, onClose, 'internalModalName'); - -// if (!timesUp) return null; - -// if (!hasBeenInteractedWith) { -// if (!hasInteractedWithModal) { -// console.log("rendering component"); -// return ( -// -//

-//
-//
-//
-//
-//
{ -// closeModal("close_clicked"); -// }} -// > -// × -//
-// {/*
*/} -//
-// -// -//
-//
-//
-//
-//
-// -// ); -// } else { -// return null; -// } -// } -// }; - - - -// const InterruptingComponent = ({ componentKind, componentName, showDelay, beforeShowingUp, onClose }) => { -// const [timesUp, setTimesUp] = useState(false); -// const [hasInteractedWithComponent, setHasInteractedWithComponent] = useState(false); -// const strapi = useContext(StrapiDataContext); - -// const markComponentAsHasBeenInteractedWith = (componentName) => { -// sessionStorage.setItem(componentKind + "_" + componentName, "true"); -// }; - -// const hasComponentBeenInteractedWith = (bannerName) => { -// return JSON.parse(sessionStorage.getItem("banner_" + bannerName)); -// }; - -// const trackBannerInteraction = (bannerName, eventDescription) => { -// gtag("event", "banner_interacted_with_" + eventDescription, { -// campaignID: bannerName, -// adType: "banner", -// }); -// }; - -// const trackBannerImpression = () => { -// console.log("We've got visibility!"); -// gtag("event", "banner_viewed", { -// campaignID: strapi.banner.internalBannerName, -// adType: "banner", -// }); -// }; - -// const shouldShow = () => { -// console.log("checking whether to show banner or not"); -// if (!strapi.banner) return false; -// if (hasBannerBeenInteractedWith(strapi.banner.internalBannerName)) -// return false; - -// let shouldShowBanner = false; - -// let noUserKindIsSet = ![ -// strapi.banner.showToReturningVisitors, -// strapi.banner.showToNewVisitors, -// strapi.banner.showToSustainers, -// strapi.banner.showToNonSustainers, -// ].some((p) => p); -// if ( -// Sefaria._uid && -// ((Sefaria.is_sustainer && strapi.banner.showToSustainers) || -// (!Sefaria.is_sustainer && strapi.banner.showToNonSustainers)) -// ) -// shouldShowBanner = true; -// else if ( -// (isReturningVisitor() && strapi.banner.showToReturningVisitors) || -// (isNewVisitor() && strapi.banner.showToNewVisitors) -// ) -// shouldShowBanner = true; -// else if (noUserKindIsSet) shouldShowBanner = true; -// if (!shouldShowBanner) return false; - -// const excludedPaths = ["/donate", "/mobile", "/app", "/ways-to-give"]; -// return excludedPaths.indexOf(window.location.pathname) === -1; -// }; - -// const closeBanner = (eventDescription) => { -// if (onClose) onClose(); -// console.log(eventDescription); -// markBannerAsHasBeenInteractedWith(strapi.banner.internalBannerName); -// setHasInteractedWithBanner(true); -// trackBannerInteraction( -// strapi.banner.internalBannerName, -// eventDescription -// ); -// }; - -// useEffect(() => { -// if (shouldShow()) { -// console.log("reaching here..."); - -// const timeoutId = setTimeout(() => { -// // s2 is the div that contains the React root and needs to be manipulated by traditional DOM methods -// if (document.getElementById("s2").classList.contains("headerOnly")) { -// document.body.classList.add("hasBannerMessage"); -// } -// setTimesUp(true); -// }, showDelay); -// return () => clearTimeout(timeoutId); // clearTimeout on component unmount -// } -// }, [strapi.banner]); // execute useEffect when the modal changes - -// if (!timesUp) return null; -// console.log("data for the component"); -// console.log(strapi.banner); - -// if (!hasInteractedWithBanner) { -// console.log("rendering component"); -// return ( -// -//
-//
-//
-// {strapi.banner.bannerText} -// -// ספריית ספריא מנגישה יותר מ-300 מיליון מלים של טקסטים יהודיים -// ברחבי העולם. לכבוד שבועות, אנא תמכו היום בספריה שמסייעת ללימוד -// שלכם על-ידי קבלת מעמד של ידידי ספריא. -// -//
-// -//
-//
×
-//
-// × -//
-//
-//
-// ); -// } else { -// return null; -// } -// }; - // InterruptingMessage.propTypes = { // messageName: PropTypes.string.isRequired, From c8200dac13d553f438c9ab490df48d87bf1a10f8 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 26 Jul 2023 15:29:58 -0400 Subject: [PATCH 21/59] Removes paragraph tag from JSX for the modal component --- static/js/Misc.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index ec2c263a99..1b871ec25e 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2223,9 +2223,7 @@ const InterruptingMessage = ({
-

-

Date: Wed, 26 Jul 2023 16:05:25 -0400 Subject: [PATCH 22/59] Moves isReturningVisitor and isNewVisitor functions etc. to be utility functions under the Sefaria object. Rewrites isNewVisitor function to have an extra condition that will be considered on mounting of ReaderApp --- static/js/Misc.jsx | 24 ++++-------------------- static/js/ReaderApp.jsx | 9 +++------ static/js/sefaria/sefaria.js | 23 +++++++++++++++++++++++ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 1b871ec25e..25c5734d22 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2086,22 +2086,6 @@ function OnInView({ children, onVisible }) { return
{children}
; } -// User could be new visitor when there isn't anything in sessionStorage either... -// Maybe don't check if it's in there or have extra conditional -function isNewVisitor() { - return ( - "isNewVisitor" in sessionStorage && - JSON.parse(sessionStorage.getItem("isNewVisitor")) - ); -} - -function isReturningVisitor() { - return ( - !isNewVisitor() && - "isReturningVisitor" in localStorage && - JSON.parse(localStorage.getItem("isReturningVisitor")) - ); -} const InterruptingMessage = ({ onClose, @@ -2166,9 +2150,9 @@ const InterruptingMessage = ({ ) shouldShowModal = true; else if ( - (isReturningVisitor() && + (Sefaria.isReturningVisitor() && strapi.modal.showToReturningVisitors) || - (isNewVisitor() && strapi.modal.showToNewVisitors) + (Sefaria.isNewVisitor() && strapi.modal.showToNewVisitors) ) shouldShowModal = true; else if (noUserKindIsSet) shouldShowModal = true; @@ -2315,8 +2299,8 @@ const Banner = ({ onClose }) => { ) shouldShowBanner = true; else if ( - (isReturningVisitor() && strapi.banner.showToReturningVisitors) || - (isNewVisitor() && strapi.banner.showToNewVisitors) + (Sefaria.isReturningVisitor() && strapi.banner.showToReturningVisitors) || + (Sefaria.isNewVisitor() && strapi.banner.showToNewVisitors) ) shouldShowBanner = true; else if (noUserKindIsSet) shouldShowBanner = true; diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index dc2d7ecdf5..71b43c965d 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -201,14 +201,11 @@ class ReaderApp extends Component { // Save all initial panels to recently viewed this.state.panels.map(this.saveLastPlace); // Initialize entries for first-time visitors to determine if they are new or returning presently or in the future - if (!("isNewVisitor" in sessionStorage) && !("isReturningVisitor" in localStorage)) { - sessionStorage.setItem("isNewVisitor", "true"); - // Setting this at this time will make the current new visitor a returning one once their session is cleared - localStorage.setItem("isReturningVisitor", "true"); + if (Sefaria.isNewVisitor()) { + Sefaria.markUserAsNewVisitor(); } else if (Sefaria._uid) { // A logged in user is automatically a returning visitor - sessionStorage.setItem("isNewVisitor", "false"); - localStorage.setItem("isReturningVisitor", "true"); + Sefaria.markUserAsReturningVisitor(); } } componentWillUnmount() { diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index e9e17cdc6b..4f2e983932 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -2254,6 +2254,29 @@ _media: {}, } Sefaria.last_place = history_item_array.filter(x=>!x.secondary).concat(Sefaria.last_place); // while technically we should remove dup. books, this list is only used on client }, + isNewVisitor: () => { + return ( + ("isNewVisitor" in sessionStorage && + JSON.parse(sessionStorage.getItem("isNewVisitor"))) || + (!("isNewVisitor" in sessionStorage) && !("isReturningVisitor" in localStorage)) + ); + }, + isReturningVisitor: () => { + return ( + !Sefaria.isNewVisitor() && + "isReturningVisitor" in localStorage && + JSON.parse(localStorage.getItem("isReturningVisitor")) + ); + }, + markUserAsNewVisitor: () => { + sessionStorage.setItem("isNewVisitor", "true"); + // Setting this at this time will make the current new visitor a returning one once their session is cleared + localStorage.setItem("isReturningVisitor", "true"); + }, + markUserAsReturningVisitor: () => { + sessionStorage.setItem("isNewVisitor", "false"); + localStorage.setItem("isReturningVisitor", "true"); + }, uploadProfilePhoto: (formData) => { return new Promise((resolve, reject) => { if (Sefaria._uid) { From e8d4dc2e5414e1e6c3c968fa289ce05739f1e5e0 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 27 Jul 2023 17:40:16 -0400 Subject: [PATCH 23/59] Revises Promotions component to create Sidebar Ads that have full support with Strapi. Component is currently being rendered too many times and the OnInView component in unable to handle that situation --- static/js/Misc.jsx | 3 +- static/js/Promotions.jsx | 257 +++++++++++-------------------- static/js/ReaderApp.jsx | 1 - static/js/context.js | 321 +++++++++++++++++++++------------------ 4 files changed, 257 insertions(+), 325 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 25c5734d22..30bdde7def 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -3269,5 +3269,6 @@ export { AdminToolHeader, CategoryChooser, TitleVariants, - requestWithCallBack + requestWithCallBack, + OnInView }; diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index 794b8b85a2..2bd3142314 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -1,13 +1,12 @@ import React, { useState, useContext, useEffect } from "react"; import { AdContext, StrapiDataProvider, StrapiDataContext } from "./context"; import classNames from "classnames"; -import { InterruptingMessage } from "./Misc"; import Sefaria from "./sefaria/sefaria"; +import { OnInView } from "./Misc"; -const Promotions = ({ adType, rerender }) => { +const Promotions = () => { const [inAppAds, setInAppAds] = useState(Sefaria._inAppAds); // local cache - const [matchingAds, setMatchingAds] = useState(null); // match the ads to what comes from the google doc - const [prevMatchingAdIds, setPrevMatchingAdIds] = useState([]); + const [matchingAds, setMatchingAds] = useState(null); // match the ads to what comes from Strapi const context = useContext(AdContext); const strapi = useContext(StrapiDataContext); useEffect(() => { @@ -25,30 +24,54 @@ const Promotions = ({ adType, rerender }) => { let keywordTargetsArray = sidebarAd.keywords .split(",") .map((x) => x.trim().toLowerCase()); + let excludeKeywordTargets = keywordTargetsArray + .filter((x) => x[0] === "!") + .map((x) => x.slice(1)); + keywordTargetsArray = keywordTargetsArray.filter( + (x) => x[0] !== "!" + ); Sefaria._inAppAds.push({ campaignId: sidebarAd.internalCampaignId, - title: sidebarAd.Title, + title: sidebarAd.title, bodyText: sidebarAd.bodyText, buttonText: sidebarAd.buttonText, - buttonUrl: sidebarAd.buttonUrl, - buttonIcon: "", - buttonLocation: sidebarAd.buttonUrl, - adType: "sidebar", + buttonURL: sidebarAd.buttonURL, + buttonIcon: sidebarAd.buttonIcon, + buttonLocation: sidebarAd.buttonAboveOrBelow, hasBlueBackground: sidebarAd.hasBlueBackground, - repetition: 5, - buttonStyle: "", trigger: { showTo: sidebarAd.showTo, - interfaceLang: Sefaria.translateISOLanguageCode( - sidebarAd.locale - ).toLowerCase(), + interfaceLang: "english", dt_start: Date.parse(sidebarAd.startTime), dt_end: Date.parse(sidebarAd.endTime), keywordTargets: keywordTargetsArray, - excludeKeywordTargets: [], + excludeKeywordTargets: excludeKeywordTargets, }, debug: sidebarAd.debug, }); + if (sidebarAd.localizations?.data?.length) { + const hebrewAttributes = sidebarAd.localizations.data[0].attributes; + const [buttonText, bodyText, buttonURL, title] = [hebrewAttributes.buttonText, hebrewAttributes.bodyText, hebrewAttributes.buttonURL, hebrewAttributes.title]; + Sefaria._inAppAds.push({ + campaignId: sidebarAd.internalCampaignId, + title: title, + bodyText: bodyText, + buttonText: buttonText, + buttonUrl: buttonURL, + buttonIcon: sidebarAd.buttonIcon, + buttonLocation: sidebarAd.buttonAboveOrBelow, + hasBlueBackground: sidebarAd.hasBlueBackground, + trigger: { + showTo: sidebarAd.showTo, + interfaceLang: "hebrew", + dt_start: Date.parse(sidebarAd.startTime), + dt_end: Date.parse(sidebarAd.endTime), + keywordTargets: keywordTargetsArray, + excludeKeywordTargets: excludeKeywordTargets, + }, + debug: sidebarAd.debug, + }); + } }); setInAppAds(Sefaria._inAppAds); } @@ -61,27 +84,6 @@ const Promotions = ({ adType, rerender }) => { setMatchingAds(getCurrentMatchingAds()); } }, [context, inAppAds]); // when state changes, the effect will run - useEffect(() => { - if (!matchingAds) { - return; - } - const matchingAdIds = matchingAds.map((x) => x.campaignId).sort(); - const newIds = matchingAdIds.filter( - (value) => !prevMatchingAdIds.includes(value) - ); - - if (newIds.length > 0) { - for (const matchingAd of matchingAds) { - if (newIds.includes(matchingAd.campaignId)) { - gtag("event", "promo_viewed", { - campaignID: matchingAd.campaignId, - adType: matchingAd.adType, - }); - } - } - setPrevMatchingAdIds(newIds); - } - }, [matchingAds]); // when state of matching ads changes, which changes in previous useEffect // function getAds() { // const url = @@ -122,7 +124,6 @@ const Promotions = ({ adType, rerender }) => { showToUser(ad) && showGivenDebugMode(ad) && ad.trigger.interfaceLang === context.interfaceLang && - ad.adType === adType && context.dt > ad.trigger.dt_start && context.dt < ad.trigger.dt_end && (context.keywordTargets.some((kw) => @@ -131,163 +132,77 @@ const Promotions = ({ adType, rerender }) => { (ad.trigger.excludeKeywordTargets.length !== 0 && !context.keywordTargets.some((kw) => ad.trigger.excludeKeywordTargets.includes(kw) - ))) && - /* line below checks if ad with particular repetition number has been seen before and is a banner */ - ((Sefaria._inBrowser && - !document.cookie.includes(`${ad.campaignId}_${ad.repetition}`)) || - ad.adType === "sidebar") + ))) ); }); } - function processSheetsData(response) { - if (response.isError()) { - alert( - "Error in query: " + - response.getMessage() + - " " + - response.getDetailedMessage() - ); - return; - } - const data = response.getDataTable(); - const columns = data.getNumberOfColumns(); - const rows = data.getNumberOfRows(); - Sefaria._inAppAds = []; - for (let r = 0; r < rows; r++) { - let row = []; - for (let c = 0; c < columns; c++) { - row.push(data.getFormattedValue(r, c)); - } - let keywordTargetsArray = row[5] - .split(",") - .map((x) => x.trim().toLowerCase()); - let excludeKeywordTargets = keywordTargetsArray.filter( - (x) => x.indexOf("!") === 0 - ); - excludeKeywordTargets = excludeKeywordTargets.map((x) => x.slice(1)); - keywordTargetsArray = keywordTargetsArray.filter( - (x) => x.indexOf("!") !== 0 - ); - Sefaria._inAppAds.push({ - campaignId: row[0], - title: row[6], - bodyText: row[7], - buttonText: row[8], - buttonUrl: row[9], - buttonIcon: row[10], - buttonLocation: row[11], - adType: row[12], - hasBlueBackground: parseInt(row[13]), - repetition: row[14], - buttonStyle: row[15], - trigger: { - showTo: row[4], - interfaceLang: row[3], - dt_start: Date.parse(row[1]), - dt_end: Date.parse(row[2]), - keywordTargets: keywordTargetsArray, - excludeKeywordTargets: excludeKeywordTargets, - }, - debug: parseInt(row[16]), - }); - } - setInAppAds(Sefaria._inAppAds); - } + console.log("promotions component is being rerendered"); - // TODO: refactor once old InterruptingMessage pattern is retired - function createBannerHtml(matchingAd) { - return `
- - ${matchingAd.bodyText} - -
-
`; - } + return matchingAds + ? matchingAds.map((ad) => ) + : null; +}; - function styleAds() { - if (adType === "banner") { - const matchingAd = matchingAds[0]; // Only allow a single banner - if (!matchingAd) { - return null; - } - // TODO: change this to use new InterruptingMessage - const bannerHtml = createBannerHtml(matchingAd); - return ( - - ); - } else { - const sidebarAds = matchingAds.map((ad) => ( - - )); - return sidebarAds; - } - } +function trackSidebarAdImpression(ad) { + console.log(ad.campaignId + " has been seen"); + gtag("event", "promo_viewed", { + campaignID: ad.campaignId, + adType: "sidebar", + }); +} - return matchingAds ? styleAds() : null; -}; +function trackSidebarAdClick(ad) { + gtag("event", "promo_clicked", { + campaignID: ad.campaignId, + adType: "sidebar", + }); +} -const SidebarAd = ({ matchingAd }) => { +const SidebarAd = ({ context, matchingAd }) => { const classes = classNames({ sidebarPromo: 1, blue: matchingAd.hasBlueBackground, }); + console.log("is this being rerendered?"); + function getButton() { return ( - gtag("event", "promo_clicked", { - campaignID: matchingAd.campaignId, - adType: matchingAd.adType, - }) - } + className="button small" + href={matchingAd.buttonURL} + onClick={() => trackSidebarAdClick(matchingAd)} > - + {matchingAd.buttonIcon?.data ? ( + + ) : null} {matchingAd.buttonText} ); } return ( -
-

{matchingAd.title}

- {matchingAd.buttonLocation === "below" ? ( - <> -

{matchingAd.bodyText}

- {getButton()} - - ) : ( - <> - {getButton()} -

{matchingAd.bodyText}

- - )} -
+ trackSidebarAdImpression(matchingAd)}> +
+

{matchingAd.title}

+ {matchingAd.buttonLocation === "below" ? ( + <> +

{matchingAd.bodyText}

+ {getButton()} + + ) : ( + <> + {getButton()} +

{matchingAd.bodyText}

+ + )} +
+
); }; diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 71b43c965d..fe8737afd6 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -1969,7 +1969,6 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { .flat() .filter(ref => !!ref); const deDupedTriggers = [...new Set(triggers.map(JSON.stringify))].map(JSON.parse).map(x => x.toLowerCase()); - // How do I get the user type? const context = { isDebug: this.props._debug, isLoggedIn: Sefaria._uid, diff --git a/static/js/context.js b/static/js/context.js index c6de85fee1..687aa8283e 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -1,7 +1,7 @@ -import React, {useContext, useEffect, useState} from "react"; +import React, { useContext, useEffect, useState } from "react"; const ContentLanguageContext = React.createContext({ - language: "english", + language: "english", }); ContentLanguageContext.displayName = "ContentLanguageContext"; //This lets us see this name in the devtools @@ -11,32 +11,30 @@ AdContext.displayName = "AdContext"; const StrapiDataContext = React.createContext({}); StrapiDataContext.displayName = "StrapiDataContext"; -function StrapiDataProvider({children}) { - const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = - useState(false); - const [strapiData, setStrapiData] = useState(null); - const [modal, setModal] = - useState(null); - const [banner, setBanner] = useState(null); - useEffect(() => { - if (STRAPI_INSTANCE) { - const getStrapiData = async () => { - try { - let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; - let getJSONDateStringInLocalTimeZone = (date) => { - let parts = getDateWithoutTime(date).split("-"); - return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); - }; - let currentDate = new Date(); - let oneWeekFromNow = new Date(); - oneWeekFromNow.setDate(currentDate.getDate() + 7); - currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC - let startDate = getJSONDateStringInLocalTimeZone(currentDate); - let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); - console.log(startDate); - console.log(endDate); - const query = - ` +function StrapiDataProvider({ children }) { + const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = + useState(false); + const [strapiData, setStrapiData] = useState(null); + const [modal, setModal] = useState(null); + const [banner, setBanner] = useState(null); + useEffect(() => { + if (STRAPI_INSTANCE) { + const getStrapiData = async () => { + try { + let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; + let getJSONDateStringInLocalTimeZone = (date) => { + let parts = getDateWithoutTime(date).split("-"); + return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); + }; + let currentDate = new Date(); + let oneWeekFromNow = new Date(); + oneWeekFromNow.setDate(currentDate.getDate() + 7); + currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC + let startDate = getJSONDateStringInLocalTimeZone(currentDate); + let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); + console.log(startDate); + console.log(endDate); + const query = ` query { banners( filters: { @@ -121,11 +119,19 @@ function StrapiDataProvider({children}) { data { id attributes { - ButtonAboveOrBelow - Title + buttonAboveOrBelow + title bodyText buttonText - buttonUrl + buttonURL + buttonIcon { + data { + attributes { + url + alternativeText + } + } + } createdAt debug endTime @@ -133,6 +139,17 @@ function StrapiDataProvider({children}) { internalCampaignId keywords locale + localizations { + data { + attributes { + locale + title + bodyText + buttonText + buttonURL + } + } + } publishedAt showTo startTime @@ -142,133 +159,133 @@ function StrapiDataProvider({children}) { } } `; - const result = fetch(STRAPI_INSTANCE + "/graphql", { - method: "POST", // *GET, POST, PUT, DELETE, etc. - mode: "cors", // no-cors, *cors, same-origin - cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", // include, *same-origin, omit - headers: { - "Content-Type": "application/json", - }, - redirect: "follow", // manual, *follow, error - referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url - body: JSON.stringify({query}), - }) - .then((response) => response.json()) - .then((result) => { - setStrapiData(result.data); - setDataFromStrapiHasBeenReceived(true); - // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists - // e.g. there are modals with overlapping time frames - let modals = result.data?.modals?.data; - console.log(modals); - let banners = result.data?.banners?.data; - console.log(banners); + const result = fetch(STRAPI_INSTANCE + "/graphql", { + method: "POST", // *GET, POST, PUT, DELETE, etc. + mode: "cors", // no-cors, *cors, same-origin + cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify({ query }), + }) + .then((response) => response.json()) + .then((result) => { + setStrapiData(result.data); + setDataFromStrapiHasBeenReceived(true); + // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists + // e.g. there are modals with overlapping time frames + let modals = result.data?.modals?.data; + console.log(modals); + let banners = result.data?.banners?.data; + console.log(banners); - const currentDate = new Date(); - if (modals?.length) { - // if they end up being sorted, the first one will be the compatible one - let modal = modals.find( - (modal) => - currentDate >= new Date(modal.attributes.modalStartDate) && - currentDate <= new Date(modal.attributes.modalEndDate) - ); - console.log("found acceptable modal:"); - console.log(modal); - if (modal) { - console.log("setting the modal"); - if (modal.attributes.localizations?.data?.length) { - let localization_attributes = modal.attributes.localizations.data[0].attributes; - let {locale, ...hebrew_attributes} = localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { - modal.attributes[attribute] = { - en: modal.attributes[attribute], - he: hebrew_attributes[attribute] - } - }); - modal.attributes.locales = ['en', 'he']; - } else { - ['modalText', 'buttonText', 'buttonURL'].forEach((attribute) => { - modal.attributes[attribute] = {en: modal.attributes[attribute], he: null}; - }); - modal.attributes.locales = ['en']; - } - setModal(modal.attributes); - } - } + const currentDate = new Date(); + if (modals?.length) { + // if they end up being sorted, the first one will be the compatible one + let modal = modals.find( + (modal) => + currentDate >= new Date(modal.attributes.modalStartDate) && + currentDate <= new Date(modal.attributes.modalEndDate) + ); + console.log("found acceptable modal:"); + console.log(modal); + if (modal) { + console.log("setting the modal"); + if (modal.attributes.localizations?.data?.length) { + let localization_attributes = + modal.attributes.localizations.data[0].attributes; + let { locale, ...hebrew_attributes } = + localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: hebrew_attributes[attribute], + }; + }); + modal.attributes.locales = ["en", "he"]; + } else { + ["modalText", "buttonText", "buttonURL"].forEach( + (attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: null, + }; + } + ); + modal.attributes.locales = ["en"]; + } + setModal(modal.attributes); + } + } - if (banners?.length) { - let banner = banners.find( - (b) => - currentDate >= new Date(b.attributes.bannerStartDate) && - currentDate <= new Date(b.attributes.bannerEndDate) - ); + if (banners?.length) { + let banner = banners.find( + (b) => + currentDate >= new Date(b.attributes.bannerStartDate) && + currentDate <= new Date(b.attributes.bannerEndDate) + ); - console.log("found acceptable banner:"); - console.log(banner); - if (banner) { - console.log("setting the banner"); - if (banner.attributes.localizations?.data?.length) { - let localization_attributes = banner.attributes.localizations.data[0].attributes; - let {locale, ...hebrew_attributes} = localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { - banner.attributes[attribute] = { - en: banner.attributes[attribute], - he: hebrew_attributes[attribute] - } - }); - banner.attributes.locales = ['en', 'he']; - } else { - // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? - ['bannerText', 'buttonText', 'buttonURL'].forEach((attribute) => { - banner.attributes[attribute] = {en: banner.attributes[attribute], he: null}; - }); - banner.attributes.locales = ['en']; - } - setBanner(banner.attributes); - console.log(banner.attributes); - } - } + console.log("found acceptable banner:"); + console.log(banner); + if (banner) { + console.log("setting the banner"); + if (banner.attributes.localizations?.data?.length) { + let localization_attributes = + banner.attributes.localizations.data[0].attributes; + let { locale, ...hebrew_attributes } = + localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + banner.attributes[attribute] = { + en: banner.attributes[attribute], + he: hebrew_attributes[attribute], + }; }); - } catch (error) { - console.error("Failed to get strapi data", error); - } - }; - getStrapiData(); - } - }, []); + banner.attributes.locales = ["en", "he"]; + } else { + // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? + ["bannerText", "buttonText", "buttonURL"].forEach( + (attribute) => { + banner.attributes[attribute] = { + en: banner.attributes[attribute], + he: null, + }; + } + ); + banner.attributes.locales = ["en"]; + } + setBanner(banner.attributes); + console.log(banner.attributes); + } + } + }); + } catch (error) { + console.error("Failed to get strapi data", error); + } + }; + getStrapiData(); + } + }, []); - return ( - - {children} - - ); + return ( + + {children} + + ); } -// function ExampleComponent() { -// const strapi = useContext(StrapiDataContext); -// if (strapi.dataFromStrapiHasBeenReceived) { -// return ( -//
-// {strapi.strapiData} -//
-// ); -// } else { -// return null; -// } -// } - export { - ContentLanguageContext, - AdContext, - StrapiDataProvider, - // ExampleComponent, - StrapiDataContext, + ContentLanguageContext, + AdContext, + StrapiDataProvider, + StrapiDataContext, }; From ea4b26dfda3528aca51c9d1c812692f77b121a2f Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 28 Jul 2023 10:50:51 -0400 Subject: [PATCH 24/59] Prevents sidebar ads from unnecessarily rerendering --- static/js/Promotions.jsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index 2bd3142314..834cfd3f0b 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -137,7 +137,6 @@ const Promotions = () => { }); } - console.log("promotions component is being rerendered"); return matchingAds ? matchingAds.map((ad) => ) @@ -159,14 +158,12 @@ function trackSidebarAdClick(ad) { }); } -const SidebarAd = ({ context, matchingAd }) => { +const SidebarAd = React.memo(({ context, matchingAd }) => { const classes = classNames({ sidebarPromo: 1, blue: matchingAd.hasBlueBackground, }); - console.log("is this being rerendered?"); - function getButton() { return ( {
); -}; +}); export { Promotions }; From bbec399bb10090b8bc06e3ee6e3dc02fb962f7e7 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 28 Jul 2023 12:34:01 -0400 Subject: [PATCH 25/59] Prevents sidebar ads from rendering until the topic data is fully loaded so impressions are accurate. Sidebar ads render too fast relative to the rest of the page. They were rendered and an impression was registered before when there was still content to be loaded. This considers the proper placement of when the sidebar ads might be outside of the viewport --- static/js/Promotions.jsx | 42 +++++++++++++++++++++++++++++----------- static/js/TopicPage.jsx | 29 ++++++++++++++------------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index 834cfd3f0b..b0a0e0ebd7 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import Sefaria from "./sefaria/sefaria"; import { OnInView } from "./Misc"; -const Promotions = () => { +const Promotions = ({ topicDataHasLoaded }) => { const [inAppAds, setInAppAds] = useState(Sefaria._inAppAds); // local cache const [matchingAds, setMatchingAds] = useState(null); // match the ads to what comes from Strapi const context = useContext(AdContext); @@ -27,9 +27,7 @@ const Promotions = () => { let excludeKeywordTargets = keywordTargetsArray .filter((x) => x[0] === "!") .map((x) => x.slice(1)); - keywordTargetsArray = keywordTargetsArray.filter( - (x) => x[0] !== "!" - ); + keywordTargetsArray = keywordTargetsArray.filter((x) => x[0] !== "!"); Sefaria._inAppAds.push({ campaignId: sidebarAd.internalCampaignId, title: sidebarAd.title, @@ -51,7 +49,12 @@ const Promotions = () => { }); if (sidebarAd.localizations?.data?.length) { const hebrewAttributes = sidebarAd.localizations.data[0].attributes; - const [buttonText, bodyText, buttonURL, title] = [hebrewAttributes.buttonText, hebrewAttributes.bodyText, hebrewAttributes.buttonURL, hebrewAttributes.title]; + const [buttonText, bodyText, buttonURL, title] = [ + hebrewAttributes.buttonText, + hebrewAttributes.bodyText, + hebrewAttributes.buttonURL, + hebrewAttributes.title, + ]; Sefaria._inAppAds.push({ campaignId: sidebarAd.internalCampaignId, title: title, @@ -137,9 +140,10 @@ const Promotions = () => { }); } - - return matchingAds - ? matchingAds.map((ad) => ) + return matchingAds && topicDataHasLoaded + ? matchingAds.map((ad) => ( + + )) : null; }; @@ -186,16 +190,32 @@ const SidebarAd = React.memo(({ context, matchingAd }) => { return ( trackSidebarAdImpression(matchingAd)}>
-

{matchingAd.title}

+

+ {matchingAd.title} +

{matchingAd.buttonLocation === "below" ? ( <> -

{matchingAd.bodyText}

+

+ {matchingAd.bodyText} +

{getButton()} ) : ( <> {getButton()} -

{matchingAd.bodyText}

+

+ {matchingAd.bodyText} +

)}
diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx index 67c150aba8..34011c1dee 100644 --- a/static/js/TopicPage.jsx +++ b/static/js/TopicPage.jsx @@ -513,19 +513,22 @@ const TopicPage = ({ : (topicData.isLoading ? : null) }
- { topicData ? - - : null } - + {topicData ? ( + <> + + + + ) : null}
From abbfe01c5eaa06af4384214734c96fad3a24516b Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 31 Jul 2023 14:40:58 -0400 Subject: [PATCH 26/59] Taking an alternative approach to showing the promotions component after the topicData has loaded --- static/js/Promotions.jsx | 4 ++-- static/js/TopicPage.jsx | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index b0a0e0ebd7..be91f75537 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -4,7 +4,7 @@ import classNames from "classnames"; import Sefaria from "./sefaria/sefaria"; import { OnInView } from "./Misc"; -const Promotions = ({ topicDataHasLoaded }) => { +const Promotions = () => { const [inAppAds, setInAppAds] = useState(Sefaria._inAppAds); // local cache const [matchingAds, setMatchingAds] = useState(null); // match the ads to what comes from Strapi const context = useContext(AdContext); @@ -140,7 +140,7 @@ const Promotions = ({ topicDataHasLoaded }) => { }); } - return matchingAds && topicDataHasLoaded + return matchingAds ? matchingAds.map((ad) => ( )) diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx index 34011c1dee..528701d19e 100644 --- a/static/js/TopicPage.jsx +++ b/static/js/TopicPage.jsx @@ -399,6 +399,7 @@ const TopicPage = ({ const [parashaData, setParashaData] = useState(null); const [showFilterHeader, setShowFilterHeader] = useState(false); const tabDisplayData = useTabDisplayData(translationLanguagePreference, versionPref); + const [isTopicSideColumnRendered, setIsTopicSideColumnRendered] = useState(false); const scrollableElement = useRef(); @@ -430,6 +431,12 @@ const TopicPage = ({ } }, [topic]); + useEffect(() => { + if (!topicData.isLoading) { + setIsTopicSideColumnRendered(true); + } + }, [topicData]); + // Set up tabs and register incremental load hooks const displayTabs = []; let onClickFilterIndex = 2; @@ -526,7 +533,7 @@ const TopicPage = ({ timePeriod={topicData.timePeriod} properties={topicData.properties} /> - + {isTopicSideColumnRendered && } ) : null}
From 1a3f49a9398aa0a61e1b4414f5bfdfc84343c8d5 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 31 Jul 2023 15:57:40 -0400 Subject: [PATCH 27/59] Prevent promotions component from rendering until topics data is loaded so that sidebar ad impressions are valid --- static/js/TopicPage.jsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx index 528701d19e..6970b947c7 100644 --- a/static/js/TopicPage.jsx +++ b/static/js/TopicPage.jsx @@ -399,7 +399,6 @@ const TopicPage = ({ const [parashaData, setParashaData] = useState(null); const [showFilterHeader, setShowFilterHeader] = useState(false); const tabDisplayData = useTabDisplayData(translationLanguagePreference, versionPref); - const [isTopicSideColumnRendered, setIsTopicSideColumnRendered] = useState(false); const scrollableElement = useRef(); @@ -431,12 +430,6 @@ const TopicPage = ({ } }, [topic]); - useEffect(() => { - if (!topicData.isLoading) { - setIsTopicSideColumnRendered(true); - } - }, [topicData]); - // Set up tabs and register incremental load hooks const displayTabs = []; let onClickFilterIndex = 2; @@ -533,7 +526,7 @@ const TopicPage = ({ timePeriod={topicData.timePeriod} properties={topicData.properties} /> - {isTopicSideColumnRendered && } + {!topicData.isLoading && } ) : null}
From d6a56a974ada600423c2164010ad16e779f22969 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 31 Jul 2023 22:40:07 -0400 Subject: [PATCH 28/59] Fixes time query range in GraphQL. Relevant content will be found that has a start date 2 weeks ago and an end date 2 weeks into the future --- static/js/context.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/static/js/context.js b/static/js/context.js index 687aa8283e..a4b2b3f537 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -27,13 +27,12 @@ function StrapiDataProvider({ children }) { return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); }; let currentDate = new Date(); - let oneWeekFromNow = new Date(); - oneWeekFromNow.setDate(currentDate.getDate() + 7); - currentDate.setDate(currentDate.getDate() - 2); // Fix time management, previous code got time 1 hour in the future in UTC - let startDate = getJSONDateStringInLocalTimeZone(currentDate); - let endDate = getJSONDateStringInLocalTimeZone(oneWeekFromNow); - console.log(startDate); - console.log(endDate); + let twoWeeksAgo = new Date(); + let twoWeeksFromNow = new Date(); + twoWeeksFromNow.setDate(currentDate.getDate() + 14); + twoWeeksAgo.setDate(currentDate.getDate() - 14); + let startDate = getJSONDateStringInLocalTimeZone(twoWeeksAgo); + let endDate = getJSONDateStringInLocalTimeZone(twoWeeksFromNow); const query = ` query { banners( From 8cf73a07d6fb13c5f42be8530ff02007f8a57339 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 1 Aug 2023 17:04:26 -0400 Subject: [PATCH 29/59] Adds linebreaks to be properly interpreted from the content that comes from Strapi --- static/css/s2.css | 2 +- static/js/Misc.jsx | 11 ++++++----- static/js/context.js | 5 +++++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/static/css/s2.css b/static/css/s2.css index d7927bde39..0a5f9d7c17 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -5797,7 +5797,7 @@ But not to use a display block directive that might break continuous mode for ot margin-bottom: 5px; color: #999; } -.textList.singlePanel .versionsTextList .topFiltersInner { +.textList.singlePanel .versionsTextList .topFiltersInner, .line-break { white-space: pre-wrap; } .showMoreFilters { diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 30bdde7def..8c6f825349 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import Component from 'react-class'; import { usePaginatedDisplay } from './Hooks'; -import {ContentLanguageContext, AdContext, StrapiDataContext} from './context'; +import {ContentLanguageContext, AdContext, StrapiDataContext, replaceNewLinesWithLinebreaks} from './context'; import ReactCrop from 'react-image-crop'; import 'react-image-crop/dist/ReactCrop.css'; import {ContentText} from "./ContentText"; @@ -60,7 +60,7 @@ const __filterChildrenByLanguage = (children, language) => { return newChildren; }; -const InterfaceText = ({text, html, markdown, children, context}) => { +const InterfaceText = ({text, html, markdown, children, context, styleClasses}) => { /** * Renders a single span for interface string with either class `int-en`` or `int-he` depending on Sefaria.interfaceLang. * If passed explicit text or html objects as props with "en" and/or "he", will only use those to determine correct text or fallback text to display. @@ -68,11 +68,12 @@ const InterfaceText = ({text, html, markdown, children, context}) => { * `children` can be the English string, which will be translated with Sefaria._ if needed. * `children` can also take the form of components above, so they can be used for longer paragrpahs or paragraphs containing html, if needed. * `context` is passed to Sefaria._ for additional translation context + * `styleClasses` are CSS classes that you want applied to all the interface languages */ const contentVariable = html ? html : markdown ? markdown : text; // assumption is `markdown` or `html` are preferred over `text` if they are present const isHebrew = Sefaria.interfaceLang === "hebrew"; - let elemclasses = classNames({"int-en": !isHebrew, "int-he": isHebrew}); + let elemclasses = classNames(styleClasses, {"int-en": !isHebrew, "int-he": isHebrew}); let textResponse = null; if (contentVariable) {// Prioritize explicit props passed in for text of the element, does not attempt to use Sefaria._() for this case. let {he, en} = contentVariable; @@ -2207,7 +2208,7 @@ const InterruptingMessage = ({
- +
{
- +
Date: Wed, 2 Aug 2023 12:38:22 -0400 Subject: [PATCH 30/59] fix(strapi-cms): Fixes showing linebreaks for Strapi content in banners/modals rendered from Markdown --- static/js/context.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/static/js/context.js b/static/js/context.js index 390f01ac68..9a16396325 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -11,9 +11,16 @@ AdContext.displayName = "AdContext"; const StrapiDataContext = React.createContext({}); StrapiDataContext.displayName = "StrapiDataContext"; -export function replaceNewLinesWithLinebreaks(string) -{ - return string.replace(/\n/gi, "  \n") +const transformValues = (obj, callback) => { + const newObj = {}; + for (let key in obj) { + newObj[key] = obj[key] !== null ? callback(obj[key]) : null; + } + return newObj; +}; + +export function replaceNewLinesWithLinebreaks(content) { + return transformValues(content, s => s.replace(/\n/gi, "  \n") + "  \n"); } function StrapiDataProvider({ children }) { From 40d7d3039f18c986e01d28f44d5a8d7afba0d5d2 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 2 Aug 2023 14:48:59 -0400 Subject: [PATCH 31/59] chore(strapi-cms): Removes old InterruptingMessage system --- reader/browsertest/framework/elements.py | 38 ----------- reader/views.py | 18 +----- sefaria/local_settings_ci.py | 1 - sefaria/local_settings_example.py | 11 ---- sefaria/model/__init__.py | 1 - sefaria/model/interrupting_message.py | 81 ------------------------ sefaria/model/user_profile.py | 18 ------ sefaria/settings.py | 30 --------- sefaria/system/context_processors.py | 1 - sefaria/urls.py | 1 - static/js/sefaria/sefaria.js | 2 - 11 files changed, 3 insertions(+), 199 deletions(-) delete mode 100644 sefaria/model/interrupting_message.py diff --git a/reader/browsertest/framework/elements.py b/reader/browsertest/framework/elements.py index 25b2f24bb3..3895328aa3 100644 --- a/reader/browsertest/framework/elements.py +++ b/reader/browsertest/framework/elements.py @@ -344,13 +344,6 @@ def scroll_nav_panel_to_bottom(self): # Initial Setup ################ - def set_modal_cookie(self): - # # set cookie to avoid popup interruption - # # We now longer set the welcomeToS2LoggedOut message by default. - # # TODO is this method still needed? YES - # pass - self.driver.add_cookie({"name": "welcomeToS2LoggedOut", "value": "true"}) - def set_cookies_cookie(self): # set cookie to avoid popup interruption # We now longer set the welcomeToS2LoggedOut message by default. @@ -364,37 +357,6 @@ def click_accept_cookies(self): except NoSuchElementException: pass - def close_modal_popup(self): - """ - :return: Boolean - did we manage to close the popup? - """ - message = self.driver.execute_script('return Sefaria.interruptingMessage') - if not message: - return True - - time.sleep(3) - try: - self.driver.find_element_by_css_selector('#interruptingMessage #interruptingMessageClose') - self.click('#interruptingMessage #interruptingMessageClose') - return True - except NoSuchElementException: - pass - - try: - self.driver.find_element_by_css_selector('#bannerMessageClose') - self.click('#bannerMessageClose') - return True - except NoSuchElementException: - return False - - def close_popup_with_accept(self): - try: - alert = self.driver.switch_to.alert - alert.accept() - except NoAlertPresentException: - print('A <> was thrown') - pass - # Login ######### diff --git a/reader/views.py b/reader/views.py index c90003d34f..64d5fb1a8b 100644 --- a/reader/views.py +++ b/reader/views.py @@ -68,7 +68,7 @@ from sefaria.image_generator import make_img_http_response import sefaria.tracker as tracker -from sefaria.settings import NODE_TIMEOUT, DEBUG, GLOBAL_INTERRUPTING_MESSAGE +from sefaria.settings import NODE_TIMEOUT, DEBUG from sefaria.model.category import TocCollectionNode from sefaria.model.abstract import SluggedAbstractMongoRecord from sefaria.utils.calendars import parashat_hashavua_and_haftara @@ -197,7 +197,6 @@ def base_props(request): if request.user.is_authenticated: profile = UserProfile(user_obj=request.user) - interrupting_message_dict = GLOBAL_INTERRUPTING_MESSAGE or {"name": profile.interrupting_message()} user_data = { "_uid": request.user.id, "_email": request.user.email, @@ -217,8 +216,7 @@ def base_props(request): "notificationCount": profile.unread_notification_count(), "notifications": profile.recent_notifications().client_contents(), "saved": {"loaded": False, "items": profile.get_history(saved=True, secondary=False, serialized=True, annotate=False)}, # saved is initially loaded without text annotations so it can quickly immediately mark any texts/sheets as saved, but marks as `loaded: false` so the full annotated data will be requested if the user visits the saved/history page - "last_place": profile.get_history(last_place=True, secondary=False, sheets=False, serialized=True), - "interruptingMessage": InterruptingMessage(attrs=interrupting_message_dict, request=request).contents(), + "last_place": profile.get_history(last_place=True, secondary=False, sheets=False, serialized=True) } else: user_data = { @@ -239,8 +237,7 @@ def base_props(request): "notificationCount": 0, "notifications": [], "saved": {"loaded": False, "items": []}, - "last_place": [], - "interruptingMessage": InterruptingMessage(attrs=GLOBAL_INTERRUPTING_MESSAGE, request=request).contents(), + "last_place": [] } user_data.update({ "last_cached": library.get_last_cached_time(), @@ -3769,15 +3766,6 @@ def my_profile(request): url += "?tab=" + request.GET.get("tab") return redirect(url) - -def interrupting_messages_read_api(request, message): - if not request.user.is_authenticated: - return jsonResponse({"error": "You must be logged in to use this API."}) - profile = UserProfile(id=request.user.id) - profile.mark_interrupting_message_read(message) - return jsonResponse({"status": "ok"}) - - @login_required @ensure_csrf_cookie def edit_profile(request): diff --git a/sefaria/local_settings_ci.py b/sefaria/local_settings_ci.py index 971c415b7c..1368222c38 100644 --- a/sefaria/local_settings_ci.py +++ b/sefaria/local_settings_ci.py @@ -48,7 +48,6 @@ MAINTENANCE_MESSAGE = "" GLOBAL_WARNING = False GLOBAL_WARNING_MESSAGE = "" -# GLOBAL_INTERRUPTING_MESSAGE = None SECRET_KEY = 'insert your long random secret key here !' diff --git a/sefaria/local_settings_example.py b/sefaria/local_settings_example.py index 3037ee7981..33103905c5 100644 --- a/sefaria/local_settings_example.py +++ b/sefaria/local_settings_example.py @@ -121,17 +121,6 @@ DOWN_FOR_MAINTENANCE = False MAINTENANCE_MESSAGE = "" -# GLOBAL_INTERRUPTING_MESSAGE = None -""" -GLOBAL_INTERRUPTING_MESSAGE = { - "name": "messageName", - "repetition": 1, - "is_fundraising": True, - "style": "modal" # "modal" or "banner" - "condition": {"returning_only": True} -} -""" - # Location of Strapi CMS instance # For local development, Strapi is located at http://localhost:1337 by default STRAPI_LOCATION = None diff --git a/sefaria/model/__init__.py b/sefaria/model/__init__.py index aec7537cf7..b5acc4cc97 100644 --- a/sefaria/model/__init__.py +++ b/sefaria/model/__init__.py @@ -24,7 +24,6 @@ from .layer import Layer, LayerSet from .notification import Notification, NotificationSet, GlobalNotification, GlobalNotificationSet from .trend import get_session_traits -from .interrupting_message import InterruptingMessage from .queue import IndexQueue, IndexQueueSet from .lock import Lock, LockSet, set_lock, release_lock, check_lock, expire_locks from .following import FollowRelationship, FollowersSet, FolloweesSet diff --git a/sefaria/model/interrupting_message.py b/sefaria/model/interrupting_message.py deleted file mode 100644 index 0daa75f920..0000000000 --- a/sefaria/model/interrupting_message.py +++ /dev/null @@ -1,81 +0,0 @@ -import json -from django.template.loader import render_to_string -from sefaria.model.user_profile import UserProfile - -class InterruptingMessage(object): - def __init__(self, attrs={}, request=None): - if attrs is None: - attrs = {} - self.name = attrs.get("name", None) - self.style = attrs.get("style", "modal") - self.repetition = attrs.get("repetition", 0) - self.is_fundraising = attrs.get("is_fundraising", False) - self.condition = attrs.get("condition", {}) - self.request = request - self.cookie_name = "%s_%d" % (self.name, self.repetition) - self.should_show = self.check_condition() - - def check_condition(self): - """Returns true if this interrupting message should be shown given its conditions""" - - # Always show to debug - if self.condition.get("debug", False): - return True - - # Nameless is useless - if not self.name: - return False - - # Don't show this name/repetiion pair more than once - if self.request.COOKIES.get(self.cookie_name, False): - return False - - # Limit to returning visitors only - if self.condition.get("returning_only", False): - if not self.request.COOKIES.get("_ga", False): - return False - - # Filter mobile traffic - if self.condition.get("desktop_only", True): - if self.request.user_agent.is_mobile: - return False - - # Filter non English interface traffic - if self.condition.get("english_only", True): - if self.request.LANGUAGE_CODE != "en": - return False - - # Filter non Hebrew interface traffic - if self.condition.get("hebrew_only", False): - if self.request.LANGUAGE_CODE != 'he': - return False - - # Filter logged out users - if self.condition.get("logged_in_only", False): - if not self.request.user.is_authenticated: - return False - - if self.is_fundraising: - if self.request.user.is_authenticated: - profile = UserProfile(id=self.request.user.id) - if(profile.is_sustainer): - return False - - return True - - def contents(self): - """ - Returns JSON for this interrupting message which may be just `null` if the - message should not be shown. - """ - - return { - "name": self.name, - "style": self.style, - "html": render_to_string("messages/%s.html" % self.name), - "repetition": self.repetition - } if self.should_show else None - - - def json(self): - return json.dumps(self.contents()) diff --git a/sefaria/model/user_profile.py b/sefaria/model/user_profile.py index e21f0784de..5571c0cfb2 100644 --- a/sefaria/model/user_profile.py +++ b/sefaria/model/user_profile.py @@ -355,7 +355,6 @@ def __init__(self, user_obj=None, id=None, slug=None, email=None, user_registrat self.twitter = "" self.linkedin = "" self.pinned_sheets = [] - self.interrupting_messages = ["newUserWelcome"] self.last_sync_web = 0 # epoch time for last sync of web app self.profile_pic_url = "" self.profile_pic_url_small = "" @@ -413,7 +412,6 @@ def __init__(self, user_obj=None, id=None, slug=None, email=None, user_registrat # create a profile for them. This allows two enviornments to share a user database, # while maintaining separate profiles (e.g. Sefaria and S4D). self.assign_slug() - self.interrupting_messages = [] self.save() @property @@ -602,21 +600,6 @@ def unread_notification_count(self): from sefaria.model.notification import NotificationSet return NotificationSet().unread_for_user(self.id).count() - def interrupting_message(self): - """ - Returns the next message to interupt the user with, if any are queued up. - """ - messages = self.interrupting_messages - return messages[0] if len(messages) > 0 else None - - def mark_interrupting_message_read(self, message): - """ - Removes `message` from the users list of queued interrupting_messages. - """ - if message in self.interrupting_messages: - self.interrupting_messages.remove(message) - self.save() - def process_history_item(self, hist, time_stamp): action = hist.pop("action", None) if self.settings.get("reading_history", True) or action == "add_saved": # regular case where history enabled, save/unsave saved item etc. or save history in either case @@ -680,7 +663,6 @@ def to_mongo_dict(self): "settings": self.settings, "version_preferences_by_corpus": self.version_preferences_by_corpus, "attr_time_stamps": self.attr_time_stamps, - "interrupting_messages": getattr(self, "interrupting_messages", []), "is_sustainer": self.is_sustainer, "tag_order": getattr(self, "tag_order", None), "last_sync_web": self.last_sync_web, diff --git a/sefaria/settings.py b/sefaria/settings.py index 40d54c4278..7eea25ff70 100644 --- a/sefaria/settings.py +++ b/sefaria/settings.py @@ -298,36 +298,6 @@ } - -# GLOBAL_INTERRUPTING_MESSAGE = { -# "name": "2023-06-16-help-center", -# "style": "banner", # "modal" or "banner" -# "repetition": 1, -# "is_fundraising": False, -# "condition": { -# "returning_only": False, -# "english_only": False, -# "desktop_only": True, -# "debug": False, -# } -# } - -GLOBAL_INTERRUPTING_MESSAGE = { - "name": "2022-04-07-passover-donate-modal", - "style": "modal", # "modal" or "banner" - "repetition": 1, - "is_fundraising": False, - "condition": { - "returning_only": False, - "english_only": False, - "desktop_only": True, - "debug": True, - } -} - -# GLOBAL_INTERRUPTING_MESSAGE = None - - # Grab environment specific settings from a file which # is left out of the repo. try: diff --git a/sefaria/system/context_processors.py b/sefaria/system/context_processors.py index cc7a71afa6..d4268c6d6e 100644 --- a/sefaria/system/context_processors.py +++ b/sefaria/system/context_processors.py @@ -13,7 +13,6 @@ from sefaria.site.site_settings import SITE_SETTINGS from sefaria.model import library from sefaria.model.user_profile import UserProfile, UserHistorySet, UserWrapper -from sefaria.model.interrupting_message import InterruptingMessage from sefaria.utils import calendars from sefaria.utils.util import short_to_long_lang_code from sefaria.utils.hebrew import hebrew_parasha_name diff --git a/sefaria/urls.py b/sefaria/urls.py index e3e6d30a75..b97e18771a 100644 --- a/sefaria/urls.py +++ b/sefaria/urls.py @@ -93,7 +93,6 @@ url(r'^api/profile/(?P[^/]+)$', reader_views.profile_get_api), url(r'^api/profile/(?P[^/]+)/(?Pfollowers|following)$', reader_views.profile_follow_api), url(r'^api/user_history/saved$', reader_views.saved_history_for_ref), - url(r'^api/interrupting-messages/read/(?P.+)$', reader_views.interrupting_messages_read_api), ] # Topics diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js index 4f2e983932..5b1c6578da 100644 --- a/static/js/sefaria/sefaria.js +++ b/static/js/sefaria/sefaria.js @@ -2997,8 +2997,6 @@ Sefaria.unpackBaseProps = function(props){ "last_place", "interfaceLang", "multiPanel", - "interruptingMessage", - "community", "followRecommendations", "trendingTopics", From 05f1d34a92fa3f9f5742bf38b56063bbe3e3992b Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 4 Aug 2023 11:31:48 -0400 Subject: [PATCH 32/59] feat(strapi-cms): Adds support for using a modal header --- static/js/Misc.jsx | 19 +++++++++++++++---- static/js/ReaderApp.jsx | 3 --- static/js/context.js | 21 +++++++++++++-------- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 8c6f825349..4262d0c544 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2208,7 +2208,18 @@ const InterruptingMessage = ({
- + {strapi.modal.modalHeader.en && ( +

{strapi.modal.modalHeader.en}

+ )} + {strapi.modal.modalHeader.he && ( +

{strapi.modal.modalHeader.he}

+ )} +
{ - closeModal("donate_button_clicked"); - }} - > + closeModal("donate_button_clicked"); + }} + > {strapi.modal.buttonText.he} diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index fe8737afd6..e101d9aa1a 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2183,9 +2183,6 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { classDict[interfaceLangClass] = true; var classes = classNames(classDict); -// const strapi = useContext(StrapiDataContext); -// const { interruptingMessageModal } = useContext(StrapiDataContext); - return ( diff --git a/static/js/context.js b/static/js/context.js index 9a16396325..090ff39ef0 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -104,12 +104,14 @@ function StrapiDataProvider({ children }) { locale buttonText buttonURL + modalHeader modalText } } } modalEndDate modalStartDate + modalHeader modalText publishedAt shouldDeployOnMobile @@ -218,14 +220,17 @@ function StrapiDataProvider({ children }) { }); modal.attributes.locales = ["en", "he"]; } else { - ["modalText", "buttonText", "buttonURL"].forEach( - (attribute) => { - modal.attributes[attribute] = { - en: modal.attributes[attribute], - he: null, - }; - } - ); + [ + "modalHeader", + "modalText", + "buttonText", + "buttonURL", + ].forEach((attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: null, + }; + }); modal.attributes.locales = ["en"]; } setModal(modal.attributes); From a5286efa802e3255fb7077f4cc86afd62ea1d53e Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 7 Aug 2023 13:49:36 -0400 Subject: [PATCH 33/59] fix(strapi-cms): Enables working with Strapi Cloud on local setup --- templates/base.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index a8f6417b17..6b6453810e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -238,7 +238,11 @@ }; {% if STRAPI_LOCATION and STRAPI_PORT %} - var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; + {% if STRAPI_PORT == 80 %} + var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}"; + {% else %} + var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; + {% endif %} {% endif %} {% endautoescape %} From 982494268f75931005f3809a3d3effe70596b8ae Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 8 Aug 2023 23:33:04 -0400 Subject: [PATCH 34/59] fix(strapi-cms): Fixes errors from cherry-picking --- static/js/Misc.jsx | 2 +- static/js/ReaderApp.jsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 4262d0c544..9cf2aa3db7 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -1,5 +1,5 @@ //const React = require('react'); -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState, useContext} from 'react'; import ReactDOM from 'react-dom'; import $ from './sefaria/sefariaJquery'; import {CollectionsModal} from "./CollectionsWidget"; diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index e101d9aa1a..92fe1e1f46 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2195,7 +2195,6 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { {panels} {sefariaModal} {communityPagePreviewControls} - {beitMidrashPanel} {/* */}
From c635e1acaaf340f2837326ae48b9e20ecf878b3b Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 9 Aug 2023 11:00:11 -0400 Subject: [PATCH 35/59] helm: Add environment variables for Strapi --- .../templates/configmap/local-settings-file.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml b/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml index 88111bcac1..fe44da8ced 100644 --- a/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml +++ b/helm-chart/sefaria-project/templates/configmap/local-settings-file.yaml @@ -311,6 +311,9 @@ data: # GLOBAL_INTERRUPTING_MESSAGE = None + STRAPI_LOCATION = os.getenv("STRAPI_LOCATION") + STRAPI_PORT = os.getenv("STRAPI_PORT") + structlog.configure( processors=[ structlog.stdlib.filter_by_level, From ec90e087c60ec50fadb9973ac6796d780194715f Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 9 Aug 2023 11:48:02 -0400 Subject: [PATCH 36/59] chore(strapi-cms): Add Strapi environment variables for CI and testing --- sefaria/local_settings_ci.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sefaria/local_settings_ci.py b/sefaria/local_settings_ci.py index 1368222c38..de5d56d849 100644 --- a/sefaria/local_settings_ci.py +++ b/sefaria/local_settings_ci.py @@ -193,6 +193,9 @@ } } +STRAPI_LOCATION = None +STRAPI_PORT = None + structlog.configure( processors=[ structlog.stdlib.filter_by_level, From 23195bc6f7198e8d110367179e88ffd0344edc94 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 9 Aug 2023 17:40:23 -0400 Subject: [PATCH 37/59] Fix error with wrong URL being used for making GraphQL requests to Strapi Cloud. Environment variable is being set as a string instead of an integer --- templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 6b6453810e..6e0f0c820c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -238,7 +238,7 @@ }; {% if STRAPI_LOCATION and STRAPI_PORT %} - {% if STRAPI_PORT == 80 %} + {% if STRAPI_PORT == "80" %} var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}"; {% else %} var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; From 7262985bfab692e55599a2cab9504bd41fbaef25 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 21 Aug 2023 21:18:28 -0400 Subject: [PATCH 38/59] Fix error handling for POST API request to GraphQL --- static/js/Promotions.jsx | 9 -- static/js/context.js | 233 ++++++++++++++++++++------------------- 2 files changed, 121 insertions(+), 121 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index be91f75537..aec8d21c36 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -88,15 +88,6 @@ const Promotions = () => { } }, [context, inAppAds]); // when state changes, the effect will run - // function getAds() { - // const url = - // 'https://docs.google.com/spreadsheets/d/1UJw2Akyv3lbLqBoZaFVWhaAp-FUQ-YZfhprL_iNhhQc/edit#gid=0' - // const query = new google.visualization.Query(url); - // query.setQuery('select A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q'); - // query.send(processSheetsData); - // - // - // } function showToUser(ad) { if (ad.trigger.showTo === "all") { diff --git a/static/js/context.js b/static/js/context.js index 090ff39ef0..3d53a9a520 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -20,7 +20,10 @@ const transformValues = (obj, callback) => { }; export function replaceNewLinesWithLinebreaks(content) { - return transformValues(content, s => s.replace(/\n/gi, "  \n") + "  \n"); + return transformValues( + content, + (s) => s.replace(/\n/gi, "  \n") + "  \n" + ); } function StrapiDataProvider({ children }) { @@ -32,20 +35,21 @@ function StrapiDataProvider({ children }) { useEffect(() => { if (STRAPI_INSTANCE) { const getStrapiData = async () => { - try { - let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; - let getJSONDateStringInLocalTimeZone = (date) => { - let parts = getDateWithoutTime(date).split("-"); - return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); - }; - let currentDate = new Date(); - let twoWeeksAgo = new Date(); - let twoWeeksFromNow = new Date(); - twoWeeksFromNow.setDate(currentDate.getDate() + 14); - twoWeeksAgo.setDate(currentDate.getDate() - 14); - let startDate = getJSONDateStringInLocalTimeZone(twoWeeksAgo); - let endDate = getJSONDateStringInLocalTimeZone(twoWeeksFromNow); - const query = ` + let getDateWithoutTime = (date) => date.toISOString().split("T")[0]; + let getJSONDateStringInLocalTimeZone = (date) => { + let parts = getDateWithoutTime(date).split("-"); + return new Date(parts[0], parts[1] - 1, parts[2]).toJSON(); + }; + let [currentDate, twoWeeksAgo, twoWeeksFromNow] = Array(3) + .fill() + .map(() => { + return new Date(); + }); + twoWeeksFromNow.setDate(currentDate.getDate() + 14); + twoWeeksAgo.setDate(currentDate.getDate() - 14); + let startDate = getJSONDateStringInLocalTimeZone(twoWeeksAgo); + let endDate = getJSONDateStringInLocalTimeZone(twoWeeksFromNow); + const query = ` query { banners( filters: { @@ -172,114 +176,119 @@ function StrapiDataProvider({ children }) { } } `; - const result = fetch(STRAPI_INSTANCE + "/graphql", { - method: "POST", // *GET, POST, PUT, DELETE, etc. - mode: "cors", // no-cors, *cors, same-origin - cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - }, - redirect: "follow", - referrerPolicy: "no-referrer", - body: JSON.stringify({ query }), + const result = fetch(STRAPI_INSTANCE + "/graphql", { + method: "POST", + mode: "cors", + cache: "no-cache", + credentials: "omit", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + body: JSON.stringify({ query }), + }) + .then((response) => { + if (!response.ok) { + throw new Error(`HTTP Error: ${response.statusText}`); + } + return response.json(); }) - .then((response) => response.json()) - .then((result) => { - setStrapiData(result.data); - setDataFromStrapiHasBeenReceived(true); - // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists - // e.g. there are modals with overlapping time frames - let modals = result.data?.modals?.data; - console.log(modals); - let banners = result.data?.banners?.data; - console.log(banners); + .then((result) => { + setStrapiData(result.data); + setDataFromStrapiHasBeenReceived(true); + // maybe sort by start date to choose which one should have a greater priority if more than one compatible one exists + // e.g. there are modals with overlapping time frames + let modals = result.data?.modals?.data; + console.log(modals); + let banners = result.data?.banners?.data; + console.log(banners); - const currentDate = new Date(); - if (modals?.length) { - // if they end up being sorted, the first one will be the compatible one - let modal = modals.find( - (modal) => - currentDate >= new Date(modal.attributes.modalStartDate) && - currentDate <= new Date(modal.attributes.modalEndDate) - ); - console.log("found acceptable modal:"); - console.log(modal); - if (modal) { - console.log("setting the modal"); - if (modal.attributes.localizations?.data?.length) { - let localization_attributes = - modal.attributes.localizations.data[0].attributes; - let { locale, ...hebrew_attributes } = - localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { - modal.attributes[attribute] = { - en: modal.attributes[attribute], - he: hebrew_attributes[attribute], - }; - }); - modal.attributes.locales = ["en", "he"]; - } else { - [ - "modalHeader", - "modalText", - "buttonText", - "buttonURL", - ].forEach((attribute) => { - modal.attributes[attribute] = { - en: modal.attributes[attribute], - he: null, - }; - }); - modal.attributes.locales = ["en"]; - } - setModal(modal.attributes); + const currentDate = new Date(); + if (modals?.length) { + // if they end up being sorted, the first one will be the compatible one + let modal = modals.find( + (modal) => + currentDate >= new Date(modal.attributes.modalStartDate) && + currentDate <= new Date(modal.attributes.modalEndDate) + ); + console.log("found acceptable modal:"); + console.log(modal); + if (modal) { + console.log("setting the modal"); + if (modal.attributes.localizations?.data?.length) { + let localization_attributes = + modal.attributes.localizations.data[0].attributes; + let { locale, ...hebrew_attributes } = + localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: hebrew_attributes[attribute], + }; + }); + modal.attributes.locales = ["en", "he"]; + } else { + [ + "modalHeader", + "modalText", + "buttonText", + "buttonURL", + ].forEach((attribute) => { + modal.attributes[attribute] = { + en: modal.attributes[attribute], + he: null, + }; + }); + modal.attributes.locales = ["en"]; } + setModal(modal.attributes); } + } - if (banners?.length) { - let banner = banners.find( - (b) => - currentDate >= new Date(b.attributes.bannerStartDate) && - currentDate <= new Date(b.attributes.bannerEndDate) - ); + if (banners?.length) { + let banner = banners.find( + (b) => + currentDate >= new Date(b.attributes.bannerStartDate) && + currentDate <= new Date(b.attributes.bannerEndDate) + ); - console.log("found acceptable banner:"); - console.log(banner); - if (banner) { - console.log("setting the banner"); - if (banner.attributes.localizations?.data?.length) { - let localization_attributes = - banner.attributes.localizations.data[0].attributes; - let { locale, ...hebrew_attributes } = - localization_attributes; - Object.keys(hebrew_attributes).forEach((attribute) => { + console.log("found acceptable banner:"); + console.log(banner); + if (banner) { + console.log("setting the banner"); + if (banner.attributes.localizations?.data?.length) { + let localization_attributes = + banner.attributes.localizations.data[0].attributes; + let { locale, ...hebrew_attributes } = + localization_attributes; + Object.keys(hebrew_attributes).forEach((attribute) => { + banner.attributes[attribute] = { + en: banner.attributes[attribute], + he: hebrew_attributes[attribute], + }; + }); + banner.attributes.locales = ["en", "he"]; + } else { + // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? + ["bannerText", "buttonText", "buttonURL"].forEach( + (attribute) => { banner.attributes[attribute] = { en: banner.attributes[attribute], - he: hebrew_attributes[attribute], + he: null, }; - }); - banner.attributes.locales = ["en", "he"]; - } else { - // Maybe have the GraphQL return null entries for each key so the same technique can be used from above? - ["bannerText", "buttonText", "buttonURL"].forEach( - (attribute) => { - banner.attributes[attribute] = { - en: banner.attributes[attribute], - he: null, - }; - } - ); - banner.attributes.locales = ["en"]; - } - setBanner(banner.attributes); - console.log(banner.attributes); + } + ); + banner.attributes.locales = ["en"]; } + setBanner(banner.attributes); + console.log(banner.attributes); } - }); - } catch (error) { - console.error("Failed to get strapi data", error); - } + } + }) + .catch((error) => { + console.error("Failed to get strapi data: ", error); + }); }; getStrapiData(); } From a7139039a240d7e9966e64a751904e89c498d35a Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 24 Aug 2023 13:20:45 -0400 Subject: [PATCH 39/59] Switch to use localStorage instead of sessionStorage for banners and modals --- static/js/Misc.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 3ba4d3cad7..0d4a2c242f 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2095,11 +2095,11 @@ const InterruptingMessage = ({ const showDelay = 5000; const markModalAsHasBeenInteractedWith = (modalName) => { - sessionStorage.setItem("modal_" + modalName, "true"); + localStorage.setItem("modal_" + modalName, "true"); }; const hasModalBeenInteractedWith = (modalName) => { - return JSON.parse(sessionStorage.getItem("modal_" + modalName)); + return JSON.parse(localStorage.getItem("modal_" + modalName)); }; const trackModalInteraction = (modalName, eventDescription) => { @@ -2265,11 +2265,11 @@ const Banner = ({ onClose }) => { const showDelay = 5000; const markBannerAsHasBeenInteractedWith = (bannerName) => { - sessionStorage.setItem("banner_" + bannerName, "true"); + localStorage.setItem("banner_" + bannerName, "true"); }; const hasBannerBeenInteractedWith = (bannerName) => { - return JSON.parse(sessionStorage.getItem("banner_" + bannerName)); + return JSON.parse(localStorage.getItem("banner_" + bannerName)); }; const trackBannerInteraction = (bannerName, eventDescription) => { From e8e60809604879db7af9c6cd0af086b3d05e2faf Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 25 Aug 2023 12:23:26 -0400 Subject: [PATCH 40/59] Make the banner compatible with the old InterruptingMessage code --- static/js/ReaderApp.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 92fe1e1f46..017607553a 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2188,8 +2188,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
- {/* */} - + */}
{header} {panels} From e44624f29e1f1e997fd4a9ff4d5a5acb4229ebf2 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 25 Aug 2023 19:00:47 -0400 Subject: [PATCH 41/59] Solve the bug where a linebreak is not at the end of the text content --- static/js/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/context.js b/static/js/context.js index 3d53a9a520..c2694f2fcc 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -22,7 +22,7 @@ const transformValues = (obj, callback) => { export function replaceNewLinesWithLinebreaks(content) { return transformValues( content, - (s) => s.replace(/\n/gi, "  \n") + "  \n" + (s) => s.replace(/\n/gi, "  \n") + "  \n  \n" ); } From 5e5baab1b293cfb6d3781d1894eb28803c2ddd8c Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 25 Aug 2023 19:01:35 -0400 Subject: [PATCH 42/59] Preserve same behavior as before for when banners are closed to handle containers --- static/js/ReaderApp.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 017607553a..b04c437228 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2188,7 +2188,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
- */} +
{header} {panels} From 4b6aa2b7f66b9341d4c0635872367da83eba66f6 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 28 Aug 2023 12:13:34 -0400 Subject: [PATCH 43/59] Rename timesUp to be more descriptive --- static/css/s2.css | 42 +++++++++++++++++------------------------ static/js/Misc.jsx | 42 ++++++++++++++++++++--------------------- static/js/ReaderApp.jsx | 8 -------- 3 files changed, 38 insertions(+), 54 deletions(-) diff --git a/static/css/s2.css b/static/css/s2.css index 0a5f9d7c17..9cb2ef9386 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -1214,63 +1214,55 @@ div.interfaceLinks-row a { margin: 0 0 30px; color: #333; } -.interface-hebrew #interruptingMessage h1{ +.interface-hebrew #interruptingMessage h1 { font-style: normal; } /* Styles used from previously existing modals */ -#highHolidayDonation { +.line-break { + white-space: pre-wrap; +} + +#defaultModal { width: 410px; max-height: 100%; max-width: 100%; } -.interface-english #highHolidayDonation { +.interface-english #defaultModal { text-align: left; } -.interface-hebrew #highHolidayDonation { +.interface-hebrew #defaultModal { text-align: right; direction: rtl; } -#highHolidayDonation p { +#defaultModalBody { color: #555; + margin-top: 0; } -.interface-hebrew p.int-en { - display: none; -} - -#highHolidayDonation p .int-en { - font-family: "adobe-garamond-pro", Georgia, serif; -} - -#highHolidayDonation p .int-he { - font-family: "adobe-garamond-pro", Georgia, serif; - /* font-family: "Heebo", sans-serif; */ +#defaultModalBody .reactMarkdown { + font-family: "adobe-garamond-pro", Georgia, serif; } -#highHolidayDonation p.sub { +#defaultModal #defaultModalBody .sub { color: #999; font-size: 12px; font-family: "Roboto", "Helvetica Neue", Helvetica, sans-serif; } -#highHolidayDonation p { - margin-top: 0; -} - -#highHolidayDonation .button { +#defaultModal .button { margin-bottom: 20px; } -#highHolidayDonation img { +#defaultModal img { max-width: 100%; } -#highHolidayDonation .buttons { +#defaultModal .buttons { text-align: right; } @@ -5797,7 +5789,7 @@ But not to use a display block directive that might break continuous mode for ot margin-bottom: 5px; color: #999; } -.textList.singlePanel .versionsTextList .topFiltersInner, .line-break { +.textList.singlePanel .versionsTextList .topFiltersInner { white-space: pre-wrap; } .showMoreFilters { diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 0d4a2c242f..522c3d4d0f 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -60,7 +60,7 @@ const __filterChildrenByLanguage = (children, language) => { return newChildren; }; -const InterfaceText = ({text, html, markdown, children, context, styleClasses}) => { +const InterfaceText = ({text, html, markdown, children, context}) => { /** * Renders a single span for interface string with either class `int-en`` or `int-he` depending on Sefaria.interfaceLang. * If passed explicit text or html objects as props with "en" and/or "he", will only use those to determine correct text or fallback text to display. @@ -68,12 +68,11 @@ const InterfaceText = ({text, html, markdown, children, context, styleClasses}) * `children` can be the English string, which will be translated with Sefaria._ if needed. * `children` can also take the form of components above, so they can be used for longer paragrpahs or paragraphs containing html, if needed. * `context` is passed to Sefaria._ for additional translation context - * `styleClasses` are CSS classes that you want applied to all the interface languages */ const contentVariable = html ? html : markdown ? markdown : text; // assumption is `markdown` or `html` are preferred over `text` if they are present const isHebrew = Sefaria.interfaceLang === "hebrew"; - let elemclasses = classNames(styleClasses, {"int-en": !isHebrew, "int-he": isHebrew}); + let elemclasses = classNames({"int-en": !isHebrew, "int-he": isHebrew}); let textResponse = null; if (contentVariable) {// Prioritize explicit props passed in for text of the element, does not attempt to use Sefaria._() for this case. let {he, en} = contentVariable; @@ -2089,7 +2088,7 @@ function OnInView({ children, onVisible }) { const InterruptingMessage = ({ onClose, }) => { - const [timesUp, setTimesUp] = useState(false); + const [interruptingMessageShowDelayHasElapsed, setInterruptingMessageShowDelayHasElapsed] = useState(false); const [hasInteractedWithModal, setHasInteractedWithModal] = useState(false); const strapi = useContext(StrapiDataContext); const showDelay = 5000; @@ -2177,13 +2176,13 @@ const InterruptingMessage = ({ useEffect(() => { if (shouldShow()) { const timeoutId = setTimeout(() => { - setTimesUp(true); + setInterruptingMessageShowDelayHasElapsed(true); }, showDelay); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } }, [strapi.modal]); // execute useEffect when the modal changes - if (!timesUp) return null; + if (!interruptingMessageShowDelayHasElapsed) return null; console.log("data for the component"); console.log(strapi.modal); @@ -2191,7 +2190,7 @@ const InterruptingMessage = ({ console.log("rendering component"); return ( -
+
@@ -2205,19 +2204,20 @@ const InterruptingMessage = ({ ×
-
+
{strapi.modal.modalHeader.en && ( -

{strapi.modal.modalHeader.en}

+

{strapi.modal.modalHeader.en}

)} {strapi.modal.modalHeader.he && ( -

{strapi.modal.modalHeader.he}

+

{strapi.modal.modalHeader.he}

)} - +
+ +
{ - const [timesUp, setTimesUp] = useState(false); + const [bannerShowDelayHasElapsed, setBannerShowDelayHasElapsed] = useState(false); const [hasInteractedWithBanner, setHasInteractedWithBanner] = useState(false); const strapi = useContext(StrapiDataContext); const showDelay = 5000; @@ -2340,13 +2340,13 @@ const Banner = ({ onClose }) => { if (document.getElementById("s2").classList.contains("headerOnly")) { document.body.classList.add("hasBannerMessage"); } - setTimesUp(true); + setBannerShowDelayHasElapsed(true); }, showDelay); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } }, [strapi.banner]); // execute useEffect when the modal changes - if (!timesUp) return null; + if (!bannerShowDelayHasElapsed) return null; console.log("data for the component"); console.log(strapi.banner); @@ -2355,10 +2355,10 @@ const Banner = ({ onClose }) => { console.log(strapi.banner.bannerText); return ( -
+
From 7bca11fcbe565e0cb63218a6371f6937768eeeed Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 28 Aug 2023 12:36:10 -0400 Subject: [PATCH 44/59] Rename to support more general button events --- static/js/Misc.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 522c3d4d0f..61f1427425 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2224,7 +2224,7 @@ const InterruptingMessage = ({ target="_blank" href={strapi.modal.buttonURL.en} onClick={() => { - closeModal("donate_button_clicked"); + closeModal("modal_button_clicked"); }} > @@ -2236,7 +2236,7 @@ const InterruptingMessage = ({ target="_blank" href={strapi.modal.buttonURL.he} onClick={() => { - closeModal("donate_button_clicked"); + closeModal("modal_button_clicked"); }} > From f9b3009cacfc807dca7b85f0afda0262f2533f04 Mon Sep 17 00:00:00 2001 From: Russel Neiss Date: Mon, 28 Aug 2023 14:17:04 -0500 Subject: [PATCH 45/59] fix: Remove duplicate adContext import --- static/js/Misc.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 80a96b131b..d10f4e3b4f 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -23,7 +23,6 @@ import Cookies from "js-cookie"; import {EditTextInfo} from "./BookPage"; import ReactMarkdown from 'react-markdown'; import TrackG4 from "./sefaria/trackG4"; -import {AdContext} from "./context"; /** * Component meant to simply denote a language specific string to go inside an InterfaceText element * ``` From 615ea333d9c2d607dc1fa6cb1ed3b4ea1ddb7346 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 28 Aug 2023 18:37:55 -0400 Subject: [PATCH 46/59] feat(strapi-cms): Add showDelay as configurable parameter for banners and modals through Strapi CMS --- static/js/Misc.jsx | 6 +++--- static/js/context.js | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 61f1427425..714f5573ea 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2055,7 +2055,7 @@ SignUpModal.propTypes = { modalContent: PropTypes.object.isRequired, }; -// Write comments explaining how this works + function OnInView({ children, onVisible }) { const elementRef = useRef(); @@ -2091,7 +2091,7 @@ const InterruptingMessage = ({ const [interruptingMessageShowDelayHasElapsed, setInterruptingMessageShowDelayHasElapsed] = useState(false); const [hasInteractedWithModal, setHasInteractedWithModal] = useState(false); const strapi = useContext(StrapiDataContext); - const showDelay = 5000; + const showDelay = strapi.modal.showDelay * 1000; const markModalAsHasBeenInteractedWith = (modalName) => { localStorage.setItem("modal_" + modalName, "true"); @@ -2262,7 +2262,7 @@ const Banner = ({ onClose }) => { const [bannerShowDelayHasElapsed, setBannerShowDelayHasElapsed] = useState(false); const [hasInteractedWithBanner, setHasInteractedWithBanner] = useState(false); const strapi = useContext(StrapiDataContext); - const showDelay = 5000; + const showDelay = strapi.banner.showDelay * 1000; const markBannerAsHasBeenInteractedWith = (bannerName) => { localStorage.setItem("banner_" + bannerName, "true"); diff --git a/static/js/context.js b/static/js/context.js index c2694f2fcc..d90071fdab 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -66,6 +66,7 @@ function StrapiDataProvider({ children }) { bannerText buttonText buttonURL + showDelay createdAt locale localizations { @@ -100,6 +101,7 @@ function StrapiDataProvider({ children }) { internalModalName buttonText buttonURL + showDelay createdAt locale localizations { From b5397c547beddca766f80731dab75ba4b24c4460 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Mon, 28 Aug 2023 19:52:11 -0400 Subject: [PATCH 47/59] fix(strapi-cms): Fix properly using the showDelay parameter from Strapi --- static/js/Misc.jsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 5b643424ec..96dd16423d 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2092,7 +2092,6 @@ const InterruptingMessage = ({ const [interruptingMessageShowDelayHasElapsed, setInterruptingMessageShowDelayHasElapsed] = useState(false); const [hasInteractedWithModal, setHasInteractedWithModal] = useState(false); const strapi = useContext(StrapiDataContext); - const showDelay = strapi.modal.showDelay * 1000; const markModalAsHasBeenInteractedWith = (modalName) => { localStorage.setItem("modal_" + modalName, "true"); @@ -2178,7 +2177,7 @@ const InterruptingMessage = ({ if (shouldShow()) { const timeoutId = setTimeout(() => { setInterruptingMessageShowDelayHasElapsed(true); - }, showDelay); + }, strapi.modal.showDelay * 1000); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } }, [strapi.modal]); // execute useEffect when the modal changes @@ -2263,7 +2262,6 @@ const Banner = ({ onClose }) => { const [bannerShowDelayHasElapsed, setBannerShowDelayHasElapsed] = useState(false); const [hasInteractedWithBanner, setHasInteractedWithBanner] = useState(false); const strapi = useContext(StrapiDataContext); - const showDelay = strapi.banner.showDelay * 1000; const markBannerAsHasBeenInteractedWith = (bannerName) => { localStorage.setItem("banner_" + bannerName, "true"); @@ -2342,7 +2340,7 @@ const Banner = ({ onClose }) => { document.body.classList.add("hasBannerMessage"); } setBannerShowDelayHasElapsed(true); - }, showDelay); + }, strapi.banner.showDelay * 1000); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } }, [strapi.banner]); // execute useEffect when the modal changes From 58abd126f929b8ab0452d3816f997a258f5d4d73 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 29 Aug 2023 15:32:39 -0400 Subject: [PATCH 48/59] chore(strapi-cms): Add .node-version to allow usage of nodenv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dcbcedb487..4cac56b0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,7 @@ node_modules ##################### venv/ .python-version +.node-version # Partner files # ################# From 9c8d3c4833f29e61830d6e81bf03c83d458537c4 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 29 Aug 2023 15:33:39 -0400 Subject: [PATCH 49/59] fix(strapi-cms): Temporary fix to allow media asset URLs to work both in production and locally --- static/js/Promotions.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index aec8d21c36..ba74d3e8e1 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -168,7 +168,10 @@ const SidebarAd = React.memo(({ context, matchingAd }) => { > {matchingAd.buttonIcon?.data ? ( From d064faadda95af3947e11753160ef8103e0b78e2 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Tue, 29 Aug 2023 19:07:33 -0400 Subject: [PATCH 50/59] docs(strapi-cms): Add lots of documentation explaining nuanced parts of the code --- static/js/Misc.jsx | 44 +++++++++++++--------------------------- static/js/Promotions.jsx | 9 ++++---- static/js/ReaderApp.jsx | 6 ++++-- static/js/TopicPage.jsx | 2 +- static/js/context.js | 30 ++++++++++++++------------- 5 files changed, 40 insertions(+), 51 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 96dd16423d..a9fd787f1f 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2056,32 +2056,43 @@ SignUpModal.propTypes = { modalContent: PropTypes.object.isRequired, }; - +// Have a callback function run when its children (element or nested group of elements) become fully visible within the viewport function OnInView({ children, onVisible }) { - const elementRef = useRef(); + // Get a mutable reference object for the child and/or children to be rendered within this component wrapped in a div + // The reference is attached to the DOM element returned from this component + // This allows us to access properties of the DOM element directly + const elementRef = useRef(); useEffect(() => { const observer = new IntersectionObserver( + // Callback function will be invoked whenever the visibility of the observed element changes (entries) => { const [entry] = entries; + // Check if the observed element is intersecting with the viewport (it's visible) + // Invoke provided prop callback for analytics purposes if (entry.isIntersecting) { onVisible(); } }, + // The entire element must be entirely visible { threshold: 1 } ); + // Start observing the element, but wait until the element exists if (elementRef.current) { observer.observe(elementRef.current); } + // Cleanup when the component unmounts return () => { + // Stop observing the element when it's no longer on the screen and can't be visible if (elementRef.current) { observer.unobserve(elementRef.current); } }; }, [onVisible]); + // Attach elementRef to a div wrapper and pass the children to be rendered within it return
{children}
; } @@ -2116,11 +2127,7 @@ const InterruptingMessage = ({ }); }; - // Need to figure out caching for Strapi so multiple queries aren't made on different page loads - // Use user context to determine whether this is valid for a user? - // Maybe user context should be used to find if there's a compatible modal const shouldShow = () => { - console.log("checking whether to show modal or not"); if (!strapi.modal) return false; if (Sefaria.interfaceLang === 'hebrew' && !strapi.modal.locales.includes('he')) return false; if ( @@ -2129,7 +2136,6 @@ const InterruptingMessage = ({ ) ) return false; - console.log('lets check a modal'); let shouldShowModal = false; @@ -2162,7 +2168,6 @@ const InterruptingMessage = ({ const closeModal = (eventDescription) => { if (onClose) onClose(); - console.log(eventDescription); markModalAsHasBeenInteractedWith( strapi.modal.internalModalName ); @@ -2183,11 +2188,8 @@ const InterruptingMessage = ({ }, [strapi.modal]); // execute useEffect when the modal changes if (!interruptingMessageShowDelayHasElapsed) return null; - console.log("data for the component"); - console.log(strapi.modal); if (!hasInteractedWithModal) { - console.log("rendering component"); return (
@@ -2279,7 +2281,6 @@ const Banner = ({ onClose }) => { }; const trackBannerImpression = () => { - console.log("We've got visibility!"); gtag("event", "banner_viewed", { campaignID: strapi.banner.internalBannerName, adType: "banner", @@ -2287,7 +2288,6 @@ const Banner = ({ onClose }) => { }; const shouldShow = () => { - console.log("checking whether to show banner or not"); if (!strapi.banner) return false; if (Sefaria.interfaceLang === 'hebrew' && !strapi.banner.locales.includes('he')) return false; if (hasBannerBeenInteractedWith(strapi.banner.internalBannerName)) @@ -2321,7 +2321,6 @@ const Banner = ({ onClose }) => { const closeBanner = (eventDescription) => { if (onClose) onClose(); - console.log(eventDescription); markBannerAsHasBeenInteractedWith(strapi.banner.internalBannerName); setHasInteractedWithBanner(true); trackBannerInteraction( @@ -2332,7 +2331,6 @@ const Banner = ({ onClose }) => { useEffect(() => { if (shouldShow()) { - console.log("reaching here..."); const timeoutId = setTimeout(() => { // s2 is the div that contains the React root and needs to be manipulated by traditional DOM methods @@ -2343,15 +2341,11 @@ const Banner = ({ onClose }) => { }, strapi.banner.showDelay * 1000); return () => clearTimeout(timeoutId); // clearTimeout on component unmount } - }, [strapi.banner]); // execute useEffect when the modal changes + }, [strapi.banner]); // execute useEffect when the banner changes if (!bannerShowDelayHasElapsed) return null; - console.log("data for the component"); - console.log(strapi.banner); if (!hasInteractedWithBanner) { - console.log("rendering banner"); - console.log(strapi.banner.bannerText); return (
@@ -2388,16 +2382,6 @@ const Banner = ({ onClose }) => { Banner.displayName = "Banner"; - -// InterruptingMessage.propTypes = { -// messageName: PropTypes.string.isRequired, -// messageHTML: PropTypes.string.isRequired, -// style: PropTypes.string.isRequired, -// repetition: PropTypes.number.isRequired, -// onClose: PropTypes.func.isRequired -// }; - - const NBox = ({ content, n, stretch, gap=0 }) => { // Wrap a list of elements into an n-column flexbox // If `stretch`, extend the final row into any remaining empty columns diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index ba74d3e8e1..a8cb3ded22 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -12,8 +12,6 @@ const Promotions = () => { useEffect(() => { if (strapi.dataFromStrapiHasBeenReceived) { Sefaria._inAppAds = []; - console.log("we got some data"); - console.log(JSON.stringify(strapi.strapiData, null, 2)); const sidebarAds = strapi.strapiData?.sidebarAds?.data; @@ -47,6 +45,7 @@ const Promotions = () => { }, debug: sidebarAd.debug, }); + // Add a separate ad if there's a Hebrew translation. There can't be an ad with only Hebrew if (sidebarAd.localizations?.data?.length) { const hebrewAttributes = sidebarAd.localizations.data[0].attributes; const [buttonText, bodyText, buttonURL, title] = [ @@ -80,13 +79,13 @@ const Promotions = () => { } } }, [strapi.dataFromStrapiHasBeenReceived]); - // empty array happens when the page loads, equivalent of didcomponentmount // dataFromStrapiHasBeenReceived will originally be null until that part is scheduled and executed + useEffect(() => { if (inAppAds) { setMatchingAds(getCurrentMatchingAds()); } - }, [context, inAppAds]); // when state changes, the effect will run + }, [context, inAppAds]); function showToUser(ad) { @@ -153,6 +152,8 @@ function trackSidebarAdClick(ad) { }); } +// Don't continuously rerender a SidebarAd if the parent component decides to rerender +// This is done to prevent multiple views from registering from OnInView const SidebarAd = React.memo(({ context, matchingAd }) => { const classes = classNames({ sidebarPromo: 1, diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 23fe95e32d..003017184f 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -2159,7 +2159,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) { {panels}
) : null; - const sefariaModal = ( + const signUpModal = (
diff --git a/static/js/context.js b/static/js/context.js index 81fe5b56e0..b75c641646 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -208,6 +208,20 @@ function StrapiDataProvider({ children }) { let modals = result.data?.modals?.data; let banners = result.data?.banners?.data; + let removeKeysFromLocalStorageWithPrefix = (prefix) => { + let keysToRemove = []; + // Removing keys while iterating affects the length of localStorage + for (let i = 0; i < localStorage.length; i++) { + let key = localStorage.key(i); + if (key.startsWith(prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach((key) => { + localStorage.removeItem(key); + }); + }; + const currentDate = new Date(); if (modals?.length) { // Only one modal can be displayed currently. The first one that matches will be the one shown @@ -217,6 +231,9 @@ function StrapiDataProvider({ children }) { currentDate <= new Date(modal.attributes.modalEndDate) ); if (modal) { + // Remove any other previous modals since there is a new modal to replace it + removeKeysFromLocalStorageWithPrefix("modal_"); + // Check if there is a Hebrew translation for the modal if (modal.attributes.localizations?.data?.length) { let localization_attributes = @@ -259,6 +276,9 @@ function StrapiDataProvider({ children }) { ); if (banner) { + // Remove any other previous banners since there is a new banner to replace it + removeKeysFromLocalStorageWithPrefix("banner_"); + // Check if there is a Hebrew translation if (banner.attributes.localizations?.data?.length) { let localization_attributes = From 724d900cf20d7d694072884ebfd6283cb87d41a5 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 00:35:12 -0400 Subject: [PATCH 53/59] Move functions to the module where they're used --- static/js/Misc.jsx | 19 +++++++++++++++---- static/js/context.js | 15 --------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index a9fd787f1f..5413064307 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -8,7 +8,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import Component from 'react-class'; import { usePaginatedDisplay } from './Hooks'; -import {ContentLanguageContext, AdContext, StrapiDataContext, replaceNewLinesWithLinebreaks} from './context'; +import {ContentLanguageContext, AdContext, StrapiDataContext} from './context'; import ReactCrop from 'react-image-crop'; import 'react-image-crop/dist/ReactCrop.css'; import {ContentText} from "./ContentText"; @@ -2058,9 +2058,6 @@ SignUpModal.propTypes = { // Have a callback function run when its children (element or nested group of elements) become fully visible within the viewport function OnInView({ children, onVisible }) { - // Get a mutable reference object for the child and/or children to be rendered within this component wrapped in a div - // The reference is attached to the DOM element returned from this component - // This allows us to access properties of the DOM element directly const elementRef = useRef(); useEffect(() => { @@ -2096,6 +2093,20 @@ function OnInView({ children, onVisible }) { return
{children}
; } +const transformValues = (obj, callback) => { + const newObj = {}; + for (let key in obj) { + newObj[key] = obj[key] !== null ? callback(obj[key]) : null; + } + return newObj; +}; + +const replaceNewLinesWithLinebreaks = (content) => { + return transformValues( + content, + (s) => s.replace(/\n/gi, "  \n") + "  \n  \n" + ); +} const InterruptingMessage = ({ onClose, diff --git a/static/js/context.js b/static/js/context.js index b75c641646..1784f811b3 100644 --- a/static/js/context.js +++ b/static/js/context.js @@ -11,21 +11,6 @@ AdContext.displayName = "AdContext"; const StrapiDataContext = React.createContext({}); StrapiDataContext.displayName = "StrapiDataContext"; -const transformValues = (obj, callback) => { - const newObj = {}; - for (let key in obj) { - newObj[key] = obj[key] !== null ? callback(obj[key]) : null; - } - return newObj; -}; - -export function replaceNewLinesWithLinebreaks(content) { - return transformValues( - content, - (s) => s.replace(/\n/gi, "  \n") + "  \n  \n" - ); -} - // Gets data from a Strapi CMS instance to be used for displaying static content function StrapiDataProvider({ children }) { const [dataFromStrapiHasBeenReceived, setDataFromStrapiHasBeenReceived] = From 94536dcf54c6fb77ea9f68996520aac49f6e5691 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 14:15:16 -0400 Subject: [PATCH 54/59] Simplify environment variable setup for local and development --- templates/base.html | 4 ---- 1 file changed, 4 deletions(-) diff --git a/templates/base.html b/templates/base.html index 6e0f0c820c..96e7ed04cd 100644 --- a/templates/base.html +++ b/templates/base.html @@ -238,11 +238,7 @@ }; {% if STRAPI_LOCATION and STRAPI_PORT %} - {% if STRAPI_PORT == "80" %} - var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}"; - {% else %} var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; - {% endif %} {% endif %} {% endautoescape %} From 97b03bce1a9404d519ea768da85d48cf784c45b8 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 15:23:54 -0400 Subject: [PATCH 55/59] Flip order of clauses. An already logged in user should be considered a returning visitor when this new feature is deployed. Everyone else will be considered a new visitor after release --- static/js/ReaderApp.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx index 003017184f..bb9f1693e9 100644 --- a/static/js/ReaderApp.jsx +++ b/static/js/ReaderApp.jsx @@ -200,12 +200,12 @@ class ReaderApp extends Component { document.addEventListener('click', this.handleInAppClickWithModifiers, {capture: true}); // Save all initial panels to recently viewed this.state.panels.map(this.saveLastPlace); - // Initialize entries for first-time visitors to determine if they are new or returning presently or in the future - if (Sefaria.isNewVisitor()) { - Sefaria.markUserAsNewVisitor(); - } else if (Sefaria._uid) { + if (Sefaria._uid) { // A logged in user is automatically a returning visitor Sefaria.markUserAsReturningVisitor(); + } else if (Sefaria.isNewVisitor()) { + // Initialize entries for first-time visitors to determine if they are new or returning presently or in the future + Sefaria.markUserAsNewVisitor(); } } componentWillUnmount() { From 31ff450537ace1d17f4e6ba61b9a2306d759b6b3 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 15:54:22 -0400 Subject: [PATCH 56/59] Revise some documentation on OnInView component --- static/js/Misc.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 5413064307..70b4ef38d3 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2056,15 +2056,20 @@ SignUpModal.propTypes = { modalContent: PropTypes.object.isRequired, }; -// Have a callback function run when its children (element or nested group of elements) become fully visible within the viewport + function OnInView({ children, onVisible }) { + /** + * Functional component that allows a callback function to run when its children become fully visible within the viewport + * `children` single element or nested group of elements wrapped in a div + * `onVisible` callback function that will be called when given component(s) are visible + */ const elementRef = useRef(); useEffect(() => { const observer = new IntersectionObserver( // Callback function will be invoked whenever the visibility of the observed element changes (entries) => { - const [entry] = entries; + const entry = entries[0]; // Check if the observed element is intersecting with the viewport (it's visible) // Invoke provided prop callback for analytics purposes if (entry.isIntersecting) { From 90dc1a1647d4b3b8aaf64a791fddef7c379e570a Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 16:07:20 -0400 Subject: [PATCH 57/59] Further revise documentation --- static/js/Misc.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/js/Misc.jsx b/static/js/Misc.jsx index 70b4ef38d3..dedb36af76 100644 --- a/static/js/Misc.jsx +++ b/static/js/Misc.jsx @@ -2059,9 +2059,10 @@ SignUpModal.propTypes = { function OnInView({ children, onVisible }) { /** - * Functional component that allows a callback function to run when its children become fully visible within the viewport + * The functional component takes an existing element and wraps it in an IntersectionObserver and returns the children, only observed and with a callback for the observer. * `children` single element or nested group of elements wrapped in a div - * `onVisible` callback function that will be called when given component(s) are visible + * `onVisible` callback function that will be called when given component(s) are visible within the viewport + * Ex. */ const elementRef = useRef(); From 04624f7d351d0ee64a9091c44ed929290f65e5ec Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Wed, 6 Sep 2023 16:46:49 -0400 Subject: [PATCH 58/59] Rename variables for clarity --- static/js/Promotions.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/static/js/Promotions.jsx b/static/js/Promotions.jsx index a8cb3ded22..b652a98863 100644 --- a/static/js/Promotions.jsx +++ b/static/js/Promotions.jsx @@ -38,8 +38,8 @@ const Promotions = () => { trigger: { showTo: sidebarAd.showTo, interfaceLang: "english", - dt_start: Date.parse(sidebarAd.startTime), - dt_end: Date.parse(sidebarAd.endTime), + startTimeDate: Date.parse(sidebarAd.startTime), + endTimeDate: Date.parse(sidebarAd.endTime), keywordTargets: keywordTargetsArray, excludeKeywordTargets: excludeKeywordTargets, }, @@ -66,8 +66,8 @@ const Promotions = () => { trigger: { showTo: sidebarAd.showTo, interfaceLang: "hebrew", - dt_start: Date.parse(sidebarAd.startTime), - dt_end: Date.parse(sidebarAd.endTime), + startTimeDate: Date.parse(sidebarAd.startTime), + endTimeDate: Date.parse(sidebarAd.endTime), keywordTargets: keywordTargetsArray, excludeKeywordTargets: excludeKeywordTargets, }, @@ -117,8 +117,8 @@ const Promotions = () => { showToUser(ad) && showGivenDebugMode(ad) && ad.trigger.interfaceLang === context.interfaceLang && - context.dt > ad.trigger.dt_start && - context.dt < ad.trigger.dt_end && + context.dt >= ad.trigger.startTimeDate && + context.dt <= ad.trigger.endTimeDate && (context.keywordTargets.some((kw) => ad.trigger.keywordTargets.includes(kw) ) || From fba253370c324a1ebec0b05461beaeaf02fc8cba Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Thu, 7 Sep 2023 13:00:16 -0400 Subject: [PATCH 59/59] Fix bug where frontend crashes if STRAPI_LOCATION and STRAPI_PORT are both set to None on the backend --- templates/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/base.html b/templates/base.html index 96e7ed04cd..71a90be76e 100644 --- a/templates/base.html +++ b/templates/base.html @@ -239,6 +239,8 @@ {% if STRAPI_LOCATION and STRAPI_PORT %} var STRAPI_INSTANCE = "{{ STRAPI_LOCATION }}:{{ STRAPI_PORT }}"; + {% else %} + var STRAPI_INSTANCE = null; {% endif %} {% endautoescape %}