From f9c07e96f07a460388bfbd13b3c93f73ef013657 Mon Sep 17 00:00:00 2001 From: Skyler Cohen Date: Fri, 23 Jun 2023 12:37:00 -0400 Subject: [PATCH 001/115] 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 002/115] 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 003/115] 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 004/115] 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 005/115] 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 006/115] 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 007/115] 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 008/115] 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 009/115] 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 010/115] 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 011/115] 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 012/115] 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 013/115] 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 014/115] 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 015/115] 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 016/115] 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 017/115] 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 018/115] 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 019/115] 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 020/115] 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 021/115] 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 022/115] 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 023/115] 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 024/115] 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 025/115] 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 026/115] 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 027/115] 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 028/115] 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 029/115] 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 030/115] 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 031/115] 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 032/115] 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 033/115] 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 034/115] 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 035/115] 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 036/115] 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 037/115] 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 038/115] 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 af8436e62102d4ca7c24dcd5e41b9a7b8768d417 Mon Sep 17 00:00:00 2001 From: stevekaplan123 Date: Tue, 22 Aug 2023 15:55:38 +0300 Subject: [PATCH 039/115] feat(Topic Editor): added birth + death + alttitles for authors --- sefaria/helper/descriptions.py | 1 + static/css/s2.css | 18 ++--- static/js/AdminEditor.jsx | 129 +++++++++++++++++++++++---------- static/js/Misc.jsx | 16 +++- static/js/TopicEditor.jsx | 20 ++++- 5 files changed, 131 insertions(+), 53 deletions(-) diff --git a/sefaria/helper/descriptions.py b/sefaria/helper/descriptions.py index 6795236453..3ee6946a78 100644 --- a/sefaria/helper/descriptions.py +++ b/sefaria/helper/descriptions.py @@ -41,6 +41,7 @@ def update_authors_data(): "Achronim": "AH", "Contemporary": "CO" } + era_slug_map = { "GN": "geon-person", "RI": "rishon-person", diff --git a/static/css/s2.css b/static/css/s2.css index 25773f7575..ba91f8b8ae 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -11208,7 +11208,7 @@ body .homeFeedWrapper.userStats { float: right; margin-right: 30px; } -#categoryChooserMenu { +.categoryChooserMenu { overflow: hidden; background: url("/static/img/arrow-down.png") 98% 20px/10px 10px no-repeat #ffffff; width: 100%; @@ -11239,7 +11239,7 @@ body .homeFeedWrapper.userStats { display: inline-block; } -#categoryChooserMenu select { +.categoryChooserMenu select { background: transparent; font-size: 16px; width: 680px; @@ -11248,14 +11248,14 @@ body .homeFeedWrapper.userStats { padding: 4px 25px 4px 10px; } -.connectionsPanel .editTextInfo #categoryChooserMenu { +.connectionsPanel .editTextInfo .categoryChooserMenu { width: 102%; } .connectionsPanel .editTextInfo .collectionsWidget { width: 70%; } -.connectionsPanel .editTextInfo #categoryChooserMenu select { +.connectionsPanel .editTextInfo .categoryChooserMenu select { width: 98.5%; } @@ -11266,11 +11266,11 @@ body .homeFeedWrapper.userStats { .searchBox .editTextInfo #newIndex { margin: 50px auto; } -.searchBox #categoryChooserMenu { +.searchBox .categoryChooserMenu { width: 97%; background: url("/static/img/arrow-down.png") 99% 20px/10px 10px no-repeat #ffffff; } -.searchBox #categoryChooserMenu select { +.searchBox .categoryChooserMenu select { width: 100%; } .searchBox .editTextInfo #newIndex input { @@ -11279,7 +11279,7 @@ body .homeFeedWrapper.userStats { .searchBox .editTextInfo #newIndex #topicDesc { width: 92%; } -#categoryChooserMenu img { +.categoryChooserMenu img { opacity: 0.43; padding: 0 5px; height: 10px; @@ -11363,14 +11363,14 @@ body .homeFeedWrapper.userStats { } @media screen and (max-width: 680px) { - #categoryChooserMenu { + .categoryChooserMenu { background: url("/static/img/arrow-down.png") 300px 20px/10px 10px no-repeat #ffffff; width: 320px; } .editTextInfo .static .headerWithButtons h1 { margin: 30px; } - #categoryChooserMenu select { + .categoryChooserMenu select { width: 340px; } .editTextInfo #newIndex input { diff --git a/static/js/AdminEditor.jsx b/static/js/AdminEditor.jsx index 0658c862cf..964f5c65b7 100644 --- a/static/js/AdminEditor.jsx +++ b/static/js/AdminEditor.jsx @@ -1,9 +1,58 @@ import React, {useRef, useState} from "react"; import Sefaria from "./sefaria/sefaria"; -import {AdminToolHeader, InterfaceText} from "./Misc"; +import {AdminToolHeader, InterfaceText, TitleVariants} from "./Misc"; import sanitizeHtml from 'sanitize-html'; import classNames from "classnames"; - +const options_for_form = { + "Title": {label: "Title", field: "enTitle", placeholder: "Add a title."}, + "Hebrew Title": {label: "Hebrew Title", field: "heTitle", placeholder: "Add a title."}, + "English Description": { + label: "English Description", + field: "enDescription", + placeholder: "Add a description.", + type: 'textarea' + }, + "Hebrew Description": { + label: "Hebrew Description", + field: "heDescription", + placeholder: "Add a description.", + type: 'textarea' + }, + "Prompt": {label: "Prompt", field: "prompt", placeholder: "Add a prompt.", textarea: true}, + "English Short Description": { + label: "English Short Description for Table of Contents", field: "enCategoryDescription", + placeholder: "Add a short description.", type: 'input' + }, + "Hebrew Short Description": { + label: "Hebrew Short Description for Table of Contents", field: "heCategoryDescription", + placeholder: "Add a short description.", type: 'input' + }, + "English Alternate Titles": { + label: "English Alternate Titles", field: "enAltTitles", + placeholder: "Add a title.", type: 'title variants' + }, + "Hebrew Alternate Titles": { + label: "Hebrew Alternate Titles", field: "heAltTitles", + placeholder: "Add a title.", type: 'title variants' + }, + "Birth Place": { + label: "Place of Birth", field: "birthPlace", placeholder: "Place of birth", type: 'input' + }, + "Death Place": { + label: "Place of Death", field: "deathPlace", placeholder: "Place of death", type: 'input' + }, + "Birth Year": { + label: "Year of Birth", field: "birthYear", placeholder: "Year of birth", type: 'input' + }, + "Death Year": { + label: "Year of Death", field: "deathYear", placeholder: "Year of death", type: 'input' + }, + "Era": { + label: "Era (GN/Gaonim, RI/Rishonim, AH/Achronim, CO/Contemporary)", field: "era", placeholder: "Choose an era", type: 'dropdown', + dropdown_data: ["GN", "RI", "AH", "CO"] + } +} + const AdminEditorButton = ({toggleAddingTopics, text, top=false, bottom=false}) => { const classes = classNames({button: 1, extraSmall: 1, topic: 1, top, bottom}); return
{ + data[field] = newTitles; + updateData({...data}); + } const preprocess = async () => { setValidatingLinks(true); for (const x of items) { @@ -85,44 +138,44 @@ const AdminEditor = ({title, data, close, catMenu, updateData, savingStatus, validate(); setValidatingLinks(false); } - const item = ({label, field, placeholder, is_textarea}) => { - return
- - {is_textarea ? - // -