diff --git a/api/api_errors.py b/api/api_errors.py
new file mode 100644
index 0000000000..aac047b48d
--- /dev/null
+++ b/api/api_errors.py
@@ -0,0 +1,16 @@
+"""
+Classes for API errors
+"""
+from sefaria.client.util import jsonResponse
+
+
+class APIInvalidInputException(Exception):
+ """
+ When data in an invalid format is passed to an API
+ """
+ def __init__(self, message):
+ super().__init__(message)
+ self.message = message
+
+ def to_json_response(self):
+ return jsonResponse({"invalid_input_error": self.message}, status=400)
diff --git a/api/views.py b/api/views.py
index bd1525390a..73d4dffa67 100644
--- a/api/views.py
+++ b/api/views.py
@@ -1,6 +1,7 @@
from sefaria.model import *
from sefaria.model.text_reuqest_adapter import TextRequestAdapter
from sefaria.client.util import jsonResponse
+from sefaria.system.exceptions import InputError, ComplexBookLevelRefError
from django.views import View
from .api_warnings import *
@@ -53,6 +54,12 @@ def get(self, request, *args, **kwargs):
if return_format not in self.RETURN_FORMATS:
return jsonResponse({'error': f'return_format should be one of those formats: {self.RETURN_FORMATS}.'}, status=400)
text_manager = TextRequestAdapter(self.oref, versions_params, fill_in_missing_segments, return_format)
- data = text_manager.get_versions_for_query()
- data = self._handle_warnings(data)
+
+ try:
+ data = text_manager.get_versions_for_query()
+ data = self._handle_warnings(data)
+
+ except Exception as e:
+ return jsonResponse({'error': str(e)}, status=400)
+
return jsonResponse(data)
diff --git a/docs/openAPI.json b/docs/openAPI.json
index e6cfb9283d..198651e12c 100644
--- a/docs/openAPI.json
+++ b/docs/openAPI.json
@@ -4460,6 +4460,24 @@
"tags": [
"Topic"
],
+ "parameters": [
+ {
+ "examples": {
+ "Return all topics": {
+ "value": "limit=0"
+ },
+ "Return 20 topics": {
+ "value": "limit=20"
+ }
+ },
+ "name": "limit",
+ "description": "This parameter limits the number of topics returned. The default is `1000`. If `limit=0` then all topics will be returned.",
+ "schema": {
+ "type": "integer"
+ },
+ "in": "query"
+ }
+ ],
"responses": {
"200": {
"content": {
diff --git a/helm-chart/sefaria-project/templates/rollout/task.yaml b/helm-chart/sefaria-project/templates/rollout/task.yaml
index 6c8e94e64f..3f9a233e59 100644
--- a/helm-chart/sefaria-project/templates/rollout/task.yaml
+++ b/helm-chart/sefaria-project/templates/rollout/task.yaml
@@ -79,6 +79,11 @@ spec:
value: "varnish-{{ .Values.deployEnv }}-{{ .Release.Revision }}"
- name: HELM_REVISION
value: "{{ .Release.Revision }}"
+ - name: SLACK_URL
+ valueFrom:
+ secretKeyRef:
+ name: { { template "sefaria.secrets.slackWebhook" . } }
+ key: slack-webhook
envFrom:
{{- if .Values.tasks.enabled }}
- secretRef:
diff --git a/helm-chart/sefaria-project/templates/rollout/web.yaml b/helm-chart/sefaria-project/templates/rollout/web.yaml
index 57c6668d6f..8b5837a08c 100644
--- a/helm-chart/sefaria-project/templates/rollout/web.yaml
+++ b/helm-chart/sefaria-project/templates/rollout/web.yaml
@@ -116,12 +116,12 @@ spec:
fieldPath: spec.nodeName
- name: OTEL_RESOURCE_ATTRIBUTES
value: k8s.container.name=app,k8s.deployment.name={{ .Values.deployEnv }}-web,k8s.namespace.name={{ .Release.Namespace }},k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME)
+ {{- end }}
- name: SLACK_URL
valueFrom:
secretKeyRef:
- name: { { template "sefaria.secrets.slackWebhook" . } }
+ name: {{ template "sefaria.secrets.slackWebhook" . }}
key: slack-webhook
- {{- end }}
envFrom:
{{- if .Values.tasks.enabled }}
- secretRef:
diff --git a/reader/views.py b/reader/views.py
index 853cf8bfe4..7828be0f34 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -55,7 +55,7 @@
from sefaria.site.site_settings import SITE_SETTINGS
from sefaria.system.multiserver.coordinator import server_coordinator
from sefaria.system.decorators import catch_error_as_json, sanitize_get_params, json_response_decorator
-from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError, DictionaryEntryNotFoundError
+from sefaria.system.exceptions import InputError, PartialRefInputError, BookNameError, NoVersionFoundError, DictionaryEntryNotFoundError, ComplexBookLevelRefError
from sefaria.system.cache import django_cache
from sefaria.system.database import db
from sefaria.helper.search import get_query_obj
@@ -1418,8 +1418,11 @@ def _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=comment
return text
if not multiple or abs(multiple) == 1:
- text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
- alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
+ try:
+ text = _get_text(oref, versionEn=versionEn, versionHe=versionHe, commentary=commentary, context=context, pad=pad,
+ alts=alts, wrapLinks=wrapLinks, layer_name=layer_name)
+ except Exception as e:
+ return jsonResponse({'error': str(e)}, status=400)
return jsonResponse(text, cb)
else:
# Return list of many sections
diff --git a/sefaria/helper/linker.py b/sefaria/helper/linker.py
index 22f1e4ab67..12e767d0bf 100644
--- a/sefaria/helper/linker.py
+++ b/sefaria/helper/linker.py
@@ -2,15 +2,56 @@
import json
import spacy
import structlog
+from cerberus import Validator
from sefaria.model.linker.ref_part import TermContext, RefPartType
from sefaria.model.linker.ref_resolver import PossiblyAmbigResolvedRef
from sefaria.model import text, library
from sefaria.model.webpage import WebPage
from sefaria.system.cache import django_cache
-from typing import List, Union, Optional, Tuple
+from api.api_errors import APIInvalidInputException
+from typing import List, Optional, Tuple
logger = structlog.get_logger(__name__)
+FIND_REFS_POST_SCHEMA = {
+ "text": {
+ "type": "dict",
+ "required": True,
+ "schema": {
+ "title": {"type": "string", "required": True},
+ "body": {"type": "string", "required": True},
+ },
+ },
+ "metaDataForTracking": {
+ "type": "dict",
+ "required": False,
+ "schema": {
+ "url": {"type": "string", "required": False},
+ "description": {"type": "string", "required": False},
+ "title": {"type": "string", "required": False},
+ },
+ },
+ "lang": {
+ "type": "string",
+ "allowed": ["he", "en"],
+ "required": False,
+ },
+ "version_preferences_by_corpus": {
+ "type": "dict",
+ "required": False,
+ "nullable": True,
+ "keysrules": {"type": "string"},
+ "valuesrules": {
+ "type": "dict",
+ "schema": {
+ "type": "string",
+ "keysrules": {"type": "string"},
+ "valuesrules": {"type": "string"},
+ },
+ },
+ },
+}
+
def load_spacy_model(path: str) -> spacy.Language:
import re, tarfile
@@ -64,13 +105,12 @@ class _FindRefsText:
body: str
lang: str
- # def __post_init__(self):
- # from sefaria.utils.hebrew import is_mostly_hebrew
- # self.lang = 'he' if is_mostly_hebrew(self.body) else 'en'
-
def _unpack_find_refs_request(request):
+ validator = Validator(FIND_REFS_POST_SCHEMA)
post_body = json.loads(request.body)
+ if not validator.validate(post_body):
+ raise APIInvalidInputException(validator.errors)
meta_data = post_body.get('metaDataForTracking')
return _create_find_refs_text(post_body), _create_find_refs_options(request.GET, post_body), meta_data
diff --git a/sefaria/helper/tests/linker_test.py b/sefaria/helper/tests/linker_test.py
index e4afc30574..8779429e77 100644
--- a/sefaria/helper/tests/linker_test.py
+++ b/sefaria/helper/tests/linker_test.py
@@ -10,6 +10,7 @@
from sefaria.model.text import Ref, TextChunk
from sefaria.model.webpage import WebPage
from sefaria.settings import ENABLE_LINKER
+from api.api_errors import APIInvalidInputException
if not ENABLE_LINKER:
pytest.skip("Linker not enabled", allow_module_level=True)
@@ -80,6 +81,12 @@ def mock_request_post_data_without_meta_data(mock_request_post_data: dict) -> di
return mock_request_post_data
+@pytest.fixture
+def mock_request_invalid_post_data(mock_request_post_data: dict) -> dict:
+ mock_request_post_data['text'] = 'plain text'
+ return mock_request_post_data
+
+
def make_mock_request(post_data: dict) -> WSGIRequest:
factory = RequestFactory()
request = factory.post('/api/find-refs', data=json.dumps(post_data), content_type='application/json')
@@ -109,6 +116,11 @@ def mock_request_without_meta_data(mock_request_post_data_without_meta_data: dic
return make_mock_request(mock_request_post_data_without_meta_data)
+@pytest.fixture
+def mock_request_invalid(mock_request_invalid_post_data: dict) -> WSGIRequest:
+ return make_mock_request(mock_request_invalid_post_data)
+
+
@pytest.fixture
def mock_webpage() -> WebPage:
# Note, the path of WebPage matches the path of the import we want to patch
@@ -162,6 +174,13 @@ def test_make_find_refs_response_without_meta_data(self, mock_request_without_me
mock_webpage.add_hit.assert_not_called()
mock_webpage.save.assert_not_called()
+ def test_make_find_refs_response_invalid_post_data(self, mock_request_invalid: dict,
+ mock_webpage: Mock):
+ with pytest.raises(APIInvalidInputException) as exc_info:
+ response = linker.make_find_refs_response(mock_request_invalid)
+ # assert that the 'text' field had a validation error
+ assert 'text' in exc_info.value.args[0]
+
class TestUnpackFindRefsRequest:
def test_unpack_find_refs_request(self, mock_request: WSGIRequest):
@@ -198,8 +217,8 @@ def mock_get_linker(self, spacy_model: spacy.Language):
with patch.object(library, 'get_linker') as mock_get_linker:
mock_linker = Mock()
mock_get_linker.return_value = mock_linker
- mock_linker.link.return_value = LinkedDoc('', [], [])
- mock_linker.link_by_paragraph.return_value = LinkedDoc('', [], [])
+ mock_linker.link.return_value = LinkedDoc('', [], [], [])
+ mock_linker.link_by_paragraph.return_value = LinkedDoc('', [], [], [])
yield mock_get_linker
def test_make_find_refs_response_linker_v3(self, mock_get_linker: WSGIRequest,
diff --git a/sefaria/model/text.py b/sefaria/model/text.py
index 3bc15026e3..6e088c9589 100644
--- a/sefaria/model/text.py
+++ b/sefaria/model/text.py
@@ -25,7 +25,7 @@
import sefaria.system.cache as scache
from sefaria.system.cache import in_memory_cache
from sefaria.system.exceptions import InputError, BookNameError, PartialRefInputError, IndexSchemaError, \
- NoVersionFoundError, DictionaryEntryNotFoundError, MissingKeyError
+ NoVersionFoundError, DictionaryEntryNotFoundError, MissingKeyError, ComplexBookLevelRefError
from sefaria.utils.hebrew import has_hebrew, is_all_hebrew, hebrew_term
from sefaria.utils.util import list_depth, truncate_string
from sefaria.datatype.jagged_array import JaggedTextArray, JaggedArray
@@ -1700,7 +1700,7 @@ def __init__(self, oref, lang, vtitle, merge_versions=False, versions=None):
elif oref.has_default_child(): #use default child:
self.oref = oref.default_child_ref()
else:
- raise InputError("Can not get TextRange at this level, please provide a more precise reference")
+ raise ComplexBookLevelRefError(book_ref=oref.normal())
self.lang = lang
self.vtitle = vtitle
self.merge_versions = merge_versions
@@ -2434,7 +2434,7 @@ def __init__(self, oref, context=1, commentary=True, version=None, lang=None,
self._alts = []
if not isinstance(oref.index_node, JaggedArrayNode) and not oref.index_node.is_virtual:
- raise InputError("Can not get TextFamily at this level, please provide a more precise reference")
+ raise InputError("Unable to find text for that ref")
for i in range(0, context):
oref = oref.context_ref()
@@ -4000,8 +4000,10 @@ def padded_ref(self):
except AttributeError: # This is a schema node, try to get a default child
if self.has_default_child():
return self.default_child_ref().padded_ref()
+ elif self.is_book_level():
+ raise ComplexBookLevelRefError(book_ref=self.normal())
else:
- raise InputError("Can not pad a schema node ref")
+ raise InputError("Cannot pad a schema node ref.")
d = self._core_dict()
if self.is_talmud():
diff --git a/sefaria/search.py b/sefaria/search.py
index ce826e2915..ef534422ed 100644
--- a/sefaria/search.py
+++ b/sefaria/search.py
@@ -502,7 +502,9 @@ def index_all(cls, index_name, debug=False, for_es=True, action=None):
total_versions = len(versions)
versions = None # release RAM
for title, vlist in list(versions_by_index.items()):
- cls.curr_index = vlist[0].get_index() if len(vlist) > 0 else None
+ if len(vlist) == 0:
+ continue
+ cls.curr_index = vlist[0].get_index()
if for_es:
cls._bulk_actions = []
try:
diff --git a/sefaria/system/exceptions.py b/sefaria/system/exceptions.py
index 72d3493f78..de010b46da 100644
--- a/sefaria/system/exceptions.py
+++ b/sefaria/system/exceptions.py
@@ -99,3 +99,14 @@ def __init__(self, method):
self.method = method
self.message = f"'{method}' is not a valid HTTP API method."
super().__init__(self.message)
+
+
+class ComplexBookLevelRefError(InputError):
+ def __init__(self, book_ref):
+ self.book_ref = book_ref
+ self.message = (f"You passed '{book_ref}', please pass a more specific ref for this book, and try again. "
+ f"'{book_ref}' is a \'complex\' book-level ref. We only support book-level "
+ f"refs in cases of texts with a 'simple' structure. To learn more about the "
+ f"structure of a text on Sefaria, "
+ f"see: https://developers.sefaria.org/docs/the-schema-of-a-simple-text")
+ super().__init__(self.message)
diff --git a/sefaria/views.py b/sefaria/views.py
index 80f3939e0d..7c0f70078c 100644
--- a/sefaria/views.py
+++ b/sefaria/views.py
@@ -47,6 +47,7 @@
from sefaria.datatype.jagged_array import JaggedTextArray
# noinspection PyUnresolvedReferences
from sefaria.system.exceptions import InputError, NoVersionFoundError
+from api.api_errors import APIInvalidInputException
from sefaria.system.database import db
from sefaria.system.decorators import catch_error_as_http
from sefaria.utils.hebrew import has_hebrew, strip_nikkud
@@ -339,7 +340,10 @@ def find_refs_report_api(request):
@api_view(["POST"])
def find_refs_api(request):
from sefaria.helper.linker import make_find_refs_response
- return jsonResponse(make_find_refs_response(request))
+ try:
+ return jsonResponse(make_find_refs_response(request))
+ except APIInvalidInputException as e:
+ return e.to_json_response()
@api_view(["GET"])
@@ -470,7 +474,7 @@ def bulktext_api(request, refs):
g = lambda x: request.GET.get(x, None)
min_char = int(g("minChar")) if g("minChar") else None
max_char = int(g("maxChar")) if g("maxChar") else None
- res = bundle_many_texts(refs, g("useTextFamily"), g("asSizedString"), min_char, max_char, g("transLangPref"), g("ven"), g("vhe"))
+ res = bundle_many_texts(refs, int(g("useTextFamily")), g("asSizedString"), min_char, max_char, g("transLangPref"), g("ven"), g("vhe"))
resp = jsonResponse(res, cb)
return resp
diff --git a/static/css/s2.css b/static/css/s2.css
index c06d921d2b..f834d933f2 100644
--- a/static/css/s2.css
+++ b/static/css/s2.css
@@ -7107,6 +7107,11 @@ But not to use a display block directive that might break continuous mode for ot
flex: 1 1 auto;
}
+.toolsButton ::before {
+ position: relative;
+ top: 3px;
+}
+
.toolsSecondaryButton {
color: var(--dark-grey);
--english-font: var(--english-sans-serif-font-family);
@@ -11525,6 +11530,19 @@ cursor: pointer;
color: white;
background-color: #18345d;
}
+.resourcesLink.studyCompanion {
+ margin-inline-start: 10px;
+}
+@media screen and (max-width: 900px) {
+ .resourcesLink.studyCompanion {
+ margin-inline-start: 0;
+ }
+}
+@media screen and (max-width: 900px) {
+ .resourcesLink.studyCompanion {
+ margin-inline-start: 0;
+ }
+}
.resourcesLink.blue img {
filter: invert(1);
opacity: 1;
@@ -13539,10 +13557,6 @@ span.ref-link-color-3 {color: blue}
}
-.productsDevBox p {
- margin-top: 0;
-}
-
.productsDevBox a::after {
content: " ›";
color: var(--commentary-blue);
diff --git a/static/font-awesome/css/font-awesome.css b/static/font-awesome/css/font-awesome.css
index a0b879fa00..9120713168 100644
--- a/static/font-awesome/css/font-awesome.css
+++ b/static/font-awesome/css/font-awesome.css
@@ -627,6 +627,10 @@
.fa-twitter:before {
content: "\f099";
}
+.fa-X:before {
+ content: "𝕏";
+ font-size: larger;
+}
.fa-facebook-f:before,
.fa-facebook:before {
content: "\f09a";
diff --git a/static/icons/email-newsletter.svg b/static/icons/email-newsletter.svg
new file mode 100644
index 0000000000..1887110e41
--- /dev/null
+++ b/static/icons/email-newsletter.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/static/js/AboutBox.jsx b/static/js/AboutBox.jsx
index 791b77e42c..1ede90b49b 100644
--- a/static/js/AboutBox.jsx
+++ b/static/js/AboutBox.jsx
@@ -5,7 +5,7 @@ import VersionBlock, {VersionsBlocksList} from './VersionBlock/VersionBlock';
import Component from 'react-class';
import {InterfaceText} from "./Misc";
import {ContentText} from "./ContentText";
-import { Modules } from './NavSidebar';
+import { SidebarModules } from './NavSidebar';
class AboutBox extends Component {
@@ -232,8 +232,8 @@ class AboutBox extends Component {
(
{versionSectionEn}{versionSectionHe}{alternateSectionHe}
) :
({versionSectionHe}{versionSectionEn}{alternateSectionHe}
)
}
-
- { !isDictionary ? : null}
+
+ { !isDictionary ? : null}
);
}
diff --git a/static/js/BookPage.jsx b/static/js/BookPage.jsx
index 4c87581a41..60060856e8 100644
--- a/static/js/BookPage.jsx
+++ b/static/js/BookPage.jsx
@@ -19,7 +19,7 @@ import React, { useState, useRef } from 'react';
import ReactDOM from 'react-dom';
import $ from './sefaria/sefariaJquery';
import Sefaria from './sefaria/sefaria';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import DictionarySearch from './DictionarySearch';
import VersionBlock from './VersionBlock/VersionBlock';
import ExtendedNotes from './ExtendedNotes';
@@ -272,7 +272,7 @@ class BookPage extends Component {
{this.props.multiPanel ? null :
-
+
}
{this.isBookToc() && ! this.props.compare ?
- : null}
+ : null}
{this.isBookToc() && ! this.props.compare ?
: null}
diff --git a/static/js/CalendarsPage.jsx b/static/js/CalendarsPage.jsx
index c6d96a8f5f..78198f0719 100644
--- a/static/js/CalendarsPage.jsx
+++ b/static/js/CalendarsPage.jsx
@@ -6,7 +6,7 @@ import React, { useState } from 'react';
import classNames from 'classnames';
import Sefaria from './sefaria/sefaria';
import $ from './sefaria/sefariaJquery';
-import { NavSidebar, Modules }from './NavSidebar';
+import { NavSidebar, SidebarModules }from './NavSidebar';
import Footer from './Footer';
import Component from 'react-class';
@@ -28,7 +28,7 @@ const CalendarsPage = ({multiPanel, initialWidth}) => {
const weeklyListings = makeListings(weeklyCalendars);
const about = multiPanel ? null :
-
+
const sidebarModules = [
multiPanel ? {type: "AboutLearningSchedules"} : {type: null},
@@ -56,7 +56,7 @@ const CalendarsPage = ({multiPanel, initialWidth}) => {
-
+
diff --git a/static/js/CollectionPage.jsx b/static/js/CollectionPage.jsx
index 49adfa7131..5fb3bfe07a 100644
--- a/static/js/CollectionPage.jsx
+++ b/static/js/CollectionPage.jsx
@@ -294,7 +294,7 @@ class CollectionPage extends Component {
{content}
-
+
diff --git a/static/js/CommunityPage.jsx b/static/js/CommunityPage.jsx
index 8510b13093..f199ec2c3f 100644
--- a/static/js/CommunityPage.jsx
+++ b/static/js/CommunityPage.jsx
@@ -4,7 +4,7 @@ import $ from './sefaria/sefariaJquery';
import Sefaria from './sefaria/sefaria';
import PropTypes from 'prop-types';
import classNames from 'classnames';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from'./Footer';
import {
InterfaceText,
@@ -62,7 +62,7 @@ const CommunityPage = ({multiPanel, toggleSignUpModal, initialWidth}) => {
-
+
@@ -103,7 +103,7 @@ const RecentlyPublished = ({multiPanel, toggleSignUpModal}) => {
recentSheets.map(s => );
const joinTheConversation = (
-
+
);
if (recentSheets) {
diff --git a/static/js/ConnectionsPanel.jsx b/static/js/ConnectionsPanel.jsx
index b874731285..039b3eadf3 100644
--- a/static/js/ConnectionsPanel.jsx
+++ b/static/js/ConnectionsPanel.jsx
@@ -1480,7 +1480,7 @@ class ShareBox extends Component {
-
+
);
diff --git a/static/js/NavSidebar.jsx b/static/js/NavSidebar.jsx
index 1aaadeaee4..38d0355549 100644
--- a/static/js/NavSidebar.jsx
+++ b/static/js/NavSidebar.jsx
@@ -7,10 +7,10 @@ import {InterfaceText, ProfileListing, Dropdown} from './Misc';
import { Promotions } from './Promotions'
import {SignUpModalKind} from "./sefaria/signupModalContent";
-const NavSidebar = ({modules}) => {
+const NavSidebar = ({sidebarModules}) => {
return
- {modules.map((m, i) =>
-
+
@@ -19,7 +19,7 @@ const NavSidebar = ({modules}) => {
};
-const Modules = ({type, props}) => {
+const SidebarModules = ({type, props}) => {
// Choose the appropriate module component to render by `type`
const moduleTypes = {
"AboutSefaria": AboutSefaria,
@@ -56,20 +56,21 @@ const Modules = ({type, props}) => {
"PortalOrganization": PortalOrganization,
"PortalNewsletter": PortalNewsletter,
"RecentlyViewed": RecentlyViewed,
+ "StudyCompanion": StudyCompanion,
};
if (!type) { return null; }
- const ModuleType = moduleTypes[type];
- return
+ const SidebarModuleType = moduleTypes[type];
+ return
};
-const Module = ({children, blue, wide}) => {
+const SidebarModule = ({children, blue, wide}) => {
const classes = classNames({navSidebarModule: 1, "sans-serif": 1, blue, wide});
return {children}
};
-const ModuleTitle = ({children, en, he, h1}) => {
+const SidebarModuleTitle = ({children, en, he, h1}) => {
const content = children ?
{children}
: ;
@@ -81,10 +82,10 @@ const ModuleTitle = ({children, en, he, h1}) => {
const TitledText = ({enTitle, heTitle, enText, heText}) => {
- return
-
+ return
+
-
+
};
const RecentlyViewedItem = ({oref}) => {
@@ -135,31 +136,46 @@ const RecentlyViewed = ({toggleSignUpModal, mobile}) => {
}
const allHistoryPhrase = mobile ? "All History" : "All history ";
const recentlyViewedList = ;
- return
+ return
{mobile && recentlyViewedList}
- ;
+ ;
}
const Promo = () =>
-
+
-
+
;
+const StudyCompanion = () => (
+
+ Study Companion
+ Get the Weekly Parashah Study Companion in your inbox.
+
+
+ Sign Up
+
+
+)
+
+
const AboutSefaria = ({hideTitle}) => (
-
- {!hideTitle ?
- A Living Library of Torah : null }
-
-
- Sefaria is home to 3,000 years of Jewish texts. We are a nonprofit organization offering free access to texts, translations,
+
+ {!hideTitle ?
+ A Living Library of Torah : null}
+
+
+ Sefaria is home to 3,000 years of Jewish texts. We are a nonprofit organization offering free access to texts, translations,
and commentaries so that everyone can participate in the ongoing process of studying, interpreting, and creating Torah.
@@ -193,7 +209,7 @@ const AboutSefaria = ({hideTitle}) => (
}
-
+
);
@@ -214,9 +230,9 @@ const AboutTranslatedText = ({translationsSlug}) => {
"yi": {title: "א לעבעדיקע ביבליאטעק פון תורה", body: "אין ספֿריאַ איז אַ היים פֿון 3,000 יאָר ייִדישע טעקסטן. מיר זענען אַ נאַן-נוץ אָרגאַניזאַציע וואָס אָפפערס פריי אַקסעס צו טעקסטן, איבערזעצונגען און קאָמענטאַרן אַזוי אַז אַלעמען קענען אָנטייל נעמען אין די אָנגאָינג פּראָצעס פון לערנען, ינטערפּריטיישאַן און שאפן תורה."}
}
return (
-
- {translationLookup[translationsSlug] ?
- translationLookup[translationsSlug]["title"] : "A Living Library of Torah"}
+
+ {translationLookup[translationsSlug] ?
+ translationLookup[translationsSlug]["title"] : "A Living Library of Torah"}
{ translationLookup[translationsSlug] ?
translationLookup[translationsSlug]["body"] :
@@ -231,13 +247,13 @@ const AboutTranslatedText = ({translationsSlug}) => {
}
-
+
);
}
const Resources = () => (
-
+
Resources
@@ -248,42 +264,42 @@ const Resources = () => (
-
+
);
const TheJewishLibrary = ({hideTitle}) => (
-
+
{!hideTitle ?
- The Jewish Library : null}
+ The Jewish Library : null}
The tradition of Torah texts is a vast, interconnected network that forms a conversation across space and time. The five books of the Torah form its foundation, and each generation of later texts functions as a commentary on those that came before it.
-
+
);
const SupportSefaria = ({blue}) => (
-
- Support Sefaria
+
+ Support Sefaria
Sefaria is an open source, nonprofit project. Support us by making a tax-deductible donation.
Make a Donation
-
+
);
const SponsorADay = () => (
-
- Sponsor A Day of Learning
+
+ Sponsor A Day of Learning
With your help, we can add more texts and translations to the library, develop new tools for learning, and keep Sefaria accessible for Torah study anytime, anywhere.
Sponsor A Day
-
+
);
@@ -298,10 +314,10 @@ const AboutTextCategory = ({cats}) => {
}
return (
-
+
-
+
);
};
@@ -327,9 +343,9 @@ const AboutText = ({index, hideTitle}) => {
if (!authors.length && !composed && !description) { return null; }
return (
-
+
{hideTitle ? null :
- About This Text }
+ About This Text }
{ composed || authors.length ?
@@ -355,7 +371,7 @@ const AboutText = ({index, hideTitle}) => {
{description ?
: null}
-
+
);
};
@@ -415,8 +431,8 @@ const DafLink = () => {
}
const Translations = () => {
- return (
- Translations
+ return (
+ Translations
Access key works from the library in several languages.
@@ -426,14 +442,14 @@ const Translations = () => {
- )
+ )
}
const LearningSchedules = () => {
return (
-
- Learning Schedules
+
+ Learning Schedules
Weekly Torah Portion :
@@ -458,15 +474,15 @@ const LearningSchedules = () => {
לוחות לימוד נוספים ›
-
+
);
};
const WeeklyTorahPortion = () => {
return (
-
- Weekly Torah Portion
+
+ Weekly Torah Portion
@@ -485,22 +501,22 @@ const WeeklyTorahPortion = () => {
פרשות השבוע ›
-
+
);
};
const DafYomi = () => {
return (
-
- Daily Learning
+
+ Daily Learning
Daf Yomi
-
+
);
};
@@ -535,8 +551,8 @@ const Visualizations = ({categories}) => {
if (links.length == 0) { return null; }
return (
-
- Visualizations
+
+ Visualizations
Explore interconnections among texts with our interactive visualizations.
{links.map((link, i) =>
@@ -552,15 +568,15 @@ const Visualizations = ({categories}) => {
תרשימים גרפיים נוספים ›
-
+
);
};
const AboutTopics = ({hideTitle}) => (
-
+
{hideTitle ? null :
- About Topics }
+ About Topics }
דפי הנושא מציגים מקורות נבחרים מארון הספרים היהודי עבור אלפי נושאים. ניתן לדפדף לפי קטגוריה או לחפש לפי נושא ספציפי, ובסרגל הצד מוצגים הנושאים הפופולריים ביותר ואלה הקשורים אליהם. הקליקו ושוטטו בין הנושאים השונים כדי ללמוד עוד.
@@ -569,19 +585,19 @@ const AboutTopics = ({hideTitle}) => (
Topics Pages present a curated selection of various genres of sources on thousands of chosen subjects. You can browse by category, search for something specific, or view the most popular topics — and related topics — on the sidebar. Explore and click through to learn more.
-
+
);
const TrendingTopics = () => (
-
- Trending Topics
+
+ Trending Topics
{Sefaria.trendingTopics.map((topic, i) =>
)}
-
+
);
@@ -594,8 +610,8 @@ const RelatedTopics = ({title}) => {
Sefaria.getIndexDetails(title).then(data => setTopics(data.relatedTopics));
},[title]);
return (topics.length ?
-
- Related Topics
+
+ Related Topics
{shownTopics.map((topic, i) =>
@@ -605,7 +621,7 @@ const RelatedTopics = ({title}) => {
{setShowMore(true);}}>
More
: null}
- : null
+ : null
);
};
@@ -614,9 +630,9 @@ const JoinTheConversation = ({wide}) => {
if (!Sefaria.multiPanel) { return null; } // Don't advertise create sheets on mobile (yet)
return (
-
+
- Join the Conversation
+ Join the Conversation
Combine sources from our library with your own comments, questions, images, and videos.
@@ -625,16 +641,16 @@ const JoinTheConversation = ({wide}) => {
Make a Sheet
-
+
);
};
const JoinTheCommunity = ({wide}) => {
return (
-
+
- Join the Conversation
+ Join the Conversation
People around the world use Sefaria to create and share Torah resources. You're invited to add your voice.
@@ -643,14 +659,14 @@ const JoinTheCommunity = ({wide}) => {
Explore the Community
-
+
);
};
const GetTheApp = () => (
-
- Get the Mobile App
+
+ Get the Mobile App
Access the Jewish library anywhere and anytime with the Sefaria mobile app.
(
platform='android'
altText={Sefaria._("Sefaria app on Android")}
/>
-
+
);
@@ -671,8 +687,8 @@ const StayConnected = () => { // TODO: remove? looks like we are not using this
const fbURL = Sefaria.interfaceLang == "hebrew" ? "https://www.facebook.com/sefaria.org.il" : "https://www.facebook.com/sefaria.org";
return (
-
- Stay Connected
+
+ Stay Connected
Get updates on new texts, learning resources, features, and more.
@@ -687,14 +703,14 @@ const StayConnected = () => { // TODO: remove? looks like we are not using this
-
+
);
};
const AboutLearningSchedules = () => (
-
- Learning Schedules
+
+ Learning Schedules
Since biblical times, the Torah has been divided into sections which are read each week on a set yearly calendar.
@@ -705,14 +721,14 @@ const AboutLearningSchedules = () => (
בעקבות המנהג הזה התפתחו לאורך השנים סדרי לימוד תקופתיים רבים נוספים, ובעזרתם יכולות קהילות וקבוצות של לומדים ללמוד יחד טקסטים שלמים.
-
+
);
const AboutCollections = ({hideTitle}) => (
-
+
{hideTitle ? null :
- About Collections }
+ About Collections }
Collections are user generated bundles of sheets which can be used privately, shared with friends, or made public on Sefaria.
אסופות הן מקבצים של דפי מקורות שנוצרו על ידי משתמשי האתר. הן ניתנות לשימוש פרטי, לצורך שיתוף עם אחרים או לשימוש ציבורי באתר ספריא.
@@ -724,13 +740,13 @@ const AboutCollections = ({hideTitle}) => (
Create a Collection
}
-
+
);
const ExploreCollections = () => (
-
- Collections
+
+ Collections
Organizations, communities and individuals around the world curate and share collections of sheets for you to explore.
-
+
);
const WhoToFollow = ({toggleSignUpModal}) => (
-
- Who to Follow
+
+ Who to Follow
{Sefaria.followRecommendations.map(user =>
)}
-
+
);
const Image = ({url}) => (
-
+
-
+
);
const Wrapper = ({title, content}) => (
-
- {title ? {title} : null}
+
+ {title ? {title} : null}
{content}
-
+
);
@@ -830,8 +846,8 @@ const DownloadVersions = ({sref}) => {
}, [sref]);
return(
-
- Download Text
+
+ Download Text
-
+
);
};
const PortalAbout = ({title, description, image_uri, image_caption}) => {
return(
-
-
+
+
-
+
)
};
const PortalMobile = ({title, description, android_link, ios_link}) => {
return(
-
+
-
+
)
};
const PortalOrganization = ({title, description}) => {
return(
-
-
+
+
{description && }
-
+
)
};
const PortalNewsletter = ({title, description}) => {
- let titleElement =
;
+ let titleElement =
;
return(
-
+
{titleElement}
{
emailPlaceholder={{en: "Email Address", he: "כתובת מייל"}}
subscribe={Sefaria.subscribeSefariaAndSteinsaltzNewsletter}
/>
-
+
)
};
export {
NavSidebar,
- Modules,
+ SidebarModules,
RecentlyViewed
};
diff --git a/static/js/NotificationsPanel.jsx b/static/js/NotificationsPanel.jsx
index 27abe785d4..567ba1e7a4 100644
--- a/static/js/NotificationsPanel.jsx
+++ b/static/js/NotificationsPanel.jsx
@@ -90,7 +90,7 @@ class NotificationsPanel extends Component {
notifications :
}
-
+
diff --git a/static/js/PublicCollectionsPage.jsx b/static/js/PublicCollectionsPage.jsx
index 5d71fd7b16..cb91d48041 100644
--- a/static/js/PublicCollectionsPage.jsx
+++ b/static/js/PublicCollectionsPage.jsx
@@ -4,7 +4,7 @@ import classNames from 'classnames';
import Footer from './Footer';
import Sefaria from './sefaria/sefaria';
import Component from 'react-class';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import {
InterfaceText,
LoadingMessage,
@@ -70,7 +70,7 @@ const PublicCollectionsPage = ({multiPanel, initialWidth}) => {
{multiPanel ? null :
- }
+ }
{ !!collectionsList ?
@@ -89,7 +89,7 @@ const PublicCollectionsPage = ({multiPanel, initialWidth}) => {
: }
-
+
diff --git a/static/js/ReaderApp.jsx b/static/js/ReaderApp.jsx
index f53f02bc2b..2218baf842 100644
--- a/static/js/ReaderApp.jsx
+++ b/static/js/ReaderApp.jsx
@@ -1010,7 +1010,7 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
showHighlight: true,
currentlyVisibleRef: refs,
}
- this.replacePanel(n-1, ref, currVersions, new_opts);
+ this.replacePanel(n-1, ref, currVersions, new_opts, false);
}
getHTMLLinkParentOfEventTarget(event){
//get the lowest level parent element of an event target that is an HTML link tag. Or Null.
@@ -1547,9 +1547,9 @@ toggleSignUpModal(modalContentKind = SignUpModalKind.Default) {
openPanelAtEnd(ref, currVersions) {
this.openPanelAt(this.state.panels.length+1, ref, currVersions);
}
- replacePanel(n, ref, currVersions, options) {
+ replacePanel(n, ref, currVersions, options, convertCommentaryRefToBaseRef=true) {
// Opens a text in in place of the panel currently open at `n`.
- this.openPanelAt(n, ref, currVersions, options, true);
+ this.openPanelAt(n, ref, currVersions, options, true, convertCommentaryRefToBaseRef);
}
openComparePanel(n, connectAfter) {
const comparePanel = this.makePanelState({
diff --git a/static/js/StaticPages.jsx b/static/js/StaticPages.jsx
index e12eaf834c..c365d3bc78 100644
--- a/static/js/StaticPages.jsx
+++ b/static/js/StaticPages.jsx
@@ -1437,8 +1437,8 @@ const DonatePage = () => (
heText=""
enButtonText="Donate Now"
heButtonText=""
- enButtonUrl="https://donate.sefaria.org/english?c_src=waystogive"
- heButtonUrl="https://donate.sefaria.org/he?c_src=waystogive"
+ enButtonUrl="https://donate.sefaria.org/give/451346/#!/donation/checkout?c_src=ways-to-give"
+ heButtonUrl="https://donate.sefaria.org/give/468442/#!/donation/checkout?c_src=ways-to-give"
borderColor="#004E5F"
/>,
(
heText=""
enButtonText="Join the Sustainers"
heButtonText=""
- enButtonUrl="https://donate.sefaria.org/sustainers?c_src=waystogive"
- heButtonUrl="https://donate.sefaria.org/sustainershe?c_src=waystogive"
+ enButtonUrl="https://donate.sefaria.org/give/457760/#!/donation/checkout?c_src=waystogive"
+ heButtonUrl="https://donate.sefaria.org/give/478929/#!/donation/checkout?c_src=waystogive"
borderColor="#97B386"
/>,
(
heText=""
enButtonText="Sponsor a Day of Learning"
heButtonText=""
- enButtonUrl="https://donate.sefaria.org/sponsor?c_src=waystogive"
- heButtonUrl="https://donate.sefaria.org/sponsorhe?c_src=waystogive"
+ enButtonUrl="https://donate.sefaria.org/campaign/sponsor-a-day-of-learning/c460961?c_src=waystogive"
+ heButtonUrl="https://donate.sefaria.org/campaign/sponsor-a-day-of-learning-hebrew/c479003?c_src=waystogive"
borderColor="#4B71B7"
/>,
(
,
@@ -1566,7 +1566,7 @@ const DonatePage = () => (
@@ -1605,7 +1605,7 @@ const DonatePage = () => (
@@ -3139,21 +3139,21 @@ const JobsPage = memo(() => {
// The static content on the page inviting users to browse our "powered-by" products
const DevBox = () => {
return (
-
-
+
+
+
- נסו את המוצרים שמפתחי תוכנה וידידי ספריא מרחבי העולם בנו עבורכם! גלו את הפרויקטים
+ נסו את המוצרים שמפתחי תוכנה וידידי ספריא מרחבי העולם בנו עבורכם! גלו את הפרויקטים
- Check out the products our software developer friends from around the world have been building for you! Explore
+ Check out the products our software developer friends from around the world have been building for you! Explore
-
-
+
+
);
};
@@ -3443,7 +3443,7 @@ const ProductsPage = memo(() => {
{products && products.length > 0 ? (
<>
{initialProducts}
- {/* */}
+
{remainingProducts}
>
) : null}
diff --git a/static/js/TextCategoryPage.jsx b/static/js/TextCategoryPage.jsx
index c8bcb4b9bb..a8e48de4e2 100644
--- a/static/js/TextCategoryPage.jsx
+++ b/static/js/TextCategoryPage.jsx
@@ -101,7 +101,7 @@ const TextCategoryPage = ({category, categories, setCategories, toggleLanguage,
initialWidth={initialWidth}
nestLevel={nestLevel} />
- {!compare ? : null}
+ {!compare ? : null}
{footer}
diff --git a/static/js/TextsPage.jsx b/static/js/TextsPage.jsx
index 53ef228dfb..8d0ce960cc 100644
--- a/static/js/TextsPage.jsx
+++ b/static/js/TextsPage.jsx
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import Sefaria from './sefaria/sefaria';
import $ from './sefaria/sefariaJquery';
-import { NavSidebar, Modules, RecentlyViewed } from './NavSidebar';
+import { NavSidebar, SidebarModules, RecentlyViewed } from './NavSidebar';
import TextCategoryPage from './TextCategoryPage';
import Footer from './Footer';
import ComparePanelHeader from './ComparePanelHeader';
@@ -79,7 +79,7 @@ const TextsPage = ({categories, settings, setCategories, onCompareBack, openSear
const about = compare || multiPanel ? null :
- ;
+ ;
const dedication = Sefaria._siteSettings.TORAH_SPECIFIC && !compare ? : null;
@@ -112,7 +112,7 @@ const TextsPage = ({categories, settings, setCategories, onCompareBack, openSear
{ !multiPanel && }
{ categoryListings }
- {!compare ? : null}
+ {!compare ? : null}
{footer}
diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx
index 99c100165f..198b974f22 100644
--- a/static/js/TopicPage.jsx
+++ b/static/js/TopicPage.jsx
@@ -261,12 +261,15 @@ const TopicCategory = ({topic, topicTitle, setTopic, setNavTopic, compare, initi
);
});
- const sidebarModules = [
+ let sidebarModules = [
{type: "AboutTopics"},
{type: "Promo"},
{type: "TrendingTopics"},
{type: "SponsorADay"},
];
+ if (topic === "torah-portions" && Sefaria.interfaceLang === "english") {
+ sidebarModules.splice(1, 0, {type: "StudyCompanion"});
+ }
return (
@@ -282,7 +285,7 @@ const TopicCategory = ({topic, topicTitle, setTopic, setNavTopic, compare, initi
-
+
@@ -440,22 +443,36 @@ return (
: null}
{tpTopImg}
{topicData && topicData.ref ?
-
-
- { topicData.parasha ? Sefaria._('Read the Portion') : topicData.ref.en }
- { topicData.parasha ? Sefaria._('Read the Portion') : norm_hebrew_ref(topicData.ref.he) }
-
- : null}
+
+ : null}
-);}
+);
+}
-const AuthorIndexItem = ({url, title, description}) => {
- return (
-
-
-
-
-
+const AuthorIndexItem = ({
+ url, title, description
+}) => {
+ return (
+
@@ -516,16 +533,16 @@ const PortalNavSideBar = ({portal, entriesToDisplayList}) => {
"organization": "PortalOrganization",
"newsletter": "PortalNewsletter"
}
- const modules = [];
+ const sidebarModules = [];
for (let key of entriesToDisplayList) {
if (!portal[key]) { continue; }
- modules.push({
+ sidebarModules.push({
type: portalModuleTypeMap[key],
props: portal[key],
});
}
return(
-
+
)
};
diff --git a/static/js/TopicPageAll.jsx b/static/js/TopicPageAll.jsx
index d732d7c0cd..9a45177c09 100644
--- a/static/js/TopicPageAll.jsx
+++ b/static/js/TopicPageAll.jsx
@@ -119,7 +119,7 @@ class TopicPageAll extends Component {
}
-
+
diff --git a/static/js/TopicsPage.jsx b/static/js/TopicsPage.jsx
index b79ab2c52c..c1b2b4fdfd 100644
--- a/static/js/TopicsPage.jsx
+++ b/static/js/TopicsPage.jsx
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames';
import Sefaria from './sefaria/sefaria';
import $ from './sefaria/sefariaJquery';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from './Footer';
import {CategoryHeader} from "./Misc";
import Component from 'react-class';
@@ -45,7 +45,7 @@ const TopicsPage = ({setNavTopic, multiPanel, initialWidth}) => {
);
const about = multiPanel ? null :
- ;
+ ;
const sidebarModules = [
multiPanel ? {type: "AboutTopics"} : {type: null},
@@ -69,7 +69,7 @@ const TopicsPage = ({setNavTopic, multiPanel, initialWidth}) => {
{ about }
{ categoryListings }
-
+
diff --git a/static/js/TranslationsPage.jsx b/static/js/TranslationsPage.jsx
index 0b74aa7ea5..7afe428cd9 100644
--- a/static/js/TranslationsPage.jsx
+++ b/static/js/TranslationsPage.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
import Sefaria from "./sefaria/sefaria";
import classNames from 'classnames';
import {InterfaceText, TabView} from './Misc';
-import { NavSidebar, Modules } from './NavSidebar';
+import { NavSidebar, SidebarModules } from './NavSidebar';
import Footer from './Footer';
@@ -89,7 +89,7 @@ const TranslationsPage = ({translationsSlug}) => {
>
}
-
+
diff --git a/static/js/UserHistoryPanel.jsx b/static/js/UserHistoryPanel.jsx
index c6aa7c30c8..1e6931c346 100644
--- a/static/js/UserHistoryPanel.jsx
+++ b/static/js/UserHistoryPanel.jsx
@@ -64,7 +64,7 @@ const UserHistoryPanel = ({menuOpen, toggleLanguage, openDisplaySettings, openNa
toggleSignUpModal={toggleSignUpModal}
key={menuOpen}/>
-
+
{footer}
diff --git a/static/js/analyticsEventTracker.js b/static/js/analyticsEventTracker.js
index 77d469fd80..5e0a43b925 100644
--- a/static/js/analyticsEventTracker.js
+++ b/static/js/analyticsEventTracker.js
@@ -3,7 +3,7 @@ const AnalyticsEventTracker = (function() {
'project', 'panel_type', 'panel_number', 'item_id', 'version', 'content_lang',
'content_id', 'content_type', 'panel_name', 'panel_category', 'position', 'ai',
'text', 'experiment', 'feature_name', 'from', 'to', 'action', 'engagement_value',
- 'engagement_type', 'logged_in', 'site_lang', 'traffic_type',
+ 'engagement_type', 'logged_in', 'site_lang', 'traffic_type', 'promotion_name'
]);
const EVENT_ATTR = 'data-anl-event';
const FIELD_ATTR_PREFIX = 'data-anl-';
@@ -226,6 +226,7 @@ const AnalyticsEventTracker = (function() {
if (entry.isIntersecting) {
const scrollIntoViewEvent = new CustomEvent(eventType, {bubbles: true});
entry.target.dispatchEvent(scrollIntoViewEvent);
+ observer.unobserve(entry.target); // Stop observing the node
}
});
});
diff --git a/static/js/linker.v3/main.js b/static/js/linker.v3/main.js
index 8a428e7053..ed2b5cc8f4 100644
--- a/static/js/linker.v3/main.js
+++ b/static/js/linker.v3/main.js
@@ -441,7 +441,8 @@ import {LinkExcluder} from "./excluder";
return new Promise((resolve, reject) => {
fetch(getFindRefsUrl(), {
method: 'POST',
- body: JSON.stringify(postData)
+ body: JSON.stringify(postData),
+ headers: {'Content-Type': 'application/json'},
})
.then(handleApiResponse)
.then(resp => resolve(resp));
diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js
index be2da1ea30..aae230fedd 100644
--- a/static/js/sefaria/sefaria.js
+++ b/static/js/sefaria/sefaria.js
@@ -1625,12 +1625,12 @@ Sefaria = extend(Sefaria, {
// First sort by predefined "top"
const hebrewTopByCategory = {
"Tanakh": ["Rashi", "Ibn Ezra", "Ramban", "Sforno"],
- "Talmud": ["Rashi", "Tosafot"],
+ "Talmud": ["Rashi", "Rashbam", "Tosafot"],
"Mishnah": ["Bartenura", "Rambam", "Ikar Tosafot Yom Tov", "Yachin", "Boaz"]
};
const englishTopByCategory = {
"Tanakh": ["Rashi", "Ibn Ezra", "Ramban", "Sforno"],
- "Talmud": ["Rashi", "Tosafot"],
+ "Talmud": ["Rashi", "Rashbam", "Tosafot"],
"Mishnah": ["Bartenura", "English Explanation of Mishnah", "Rambam", "Ikar Tosafot Yom Tov", "Yachin", "Boaz"]
};
const top = (byHebrew ? hebrewTopByCategory[category] : englishTopByCategory[category]) || [];
diff --git a/templates/static/he/ways-to-give.html b/templates/static/he/ways-to-give.html
index 1101c7639c..6eb6051dc2 100644
--- a/templates/static/he/ways-to-give.html
+++ b/templates/static/he/ways-to-give.html
@@ -42,7 +42,7 @@