(
heText=""
enButtonText="Join Now or Learn More"
heButtonText=""
- enButtonUrl="https://donate.sefaria.org/campaign/giving-circles/c557214"
+ enButtonUrl="https://donate.sefaria.org/campaign/giving-circles/c557214?c_src=waystogive"
heButtonUrl=""
borderColor="#7C416F"
/>
@@ -1491,7 +1493,7 @@ const DonatePage = () => (
,
@@ -1603,7 +1605,7 @@ const DonatePage = () => (
@@ -3021,6 +3023,7 @@ const NoJobsNotice = () => {
const JobsPage = memo(() => {
const [groupedJobPostings, setGroupedJobPostings] = useState({});
const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(true);
const fetchJobsJSON = async () => {
const currentDateTime = new Date().toISOString();
@@ -3069,6 +3072,7 @@ const JobsPage = memo(() => {
};
const loadJobPostings = async () => {
+ setLoading(true);
if (typeof STRAPI_INSTANCE !== "undefined" && STRAPI_INSTANCE) {
try {
const jobsData = await fetchJobsJSON();
@@ -3102,20 +3106,27 @@ const JobsPage = memo(() => {
} else {
setError("Error: Sefaria's CMS cannot be reached");
}
+ setLoading(false);
};
useEffect(() => {
loadJobPostings();
}, []);
+ const jobsAvailable = Object.keys(groupedJobPostings)?.length;
return (
{error ? (
{error}
+ ) : loading ? (
+ <>
+
+
+ >
) : (
<>
-
- {Object.keys(groupedJobPostings)?.length ? (
+
+ {jobsAvailable ? (
) : (
diff --git a/static/js/Story.jsx b/static/js/Story.jsx
index 12bf3df64c..8aaacc0d89 100644
--- a/static/js/Story.jsx
+++ b/static/js/Story.jsx
@@ -170,14 +170,14 @@ const ReviewStateIndicatorLang = ({reviewState, markReviewed}) => {
}
const markReviewedPostRequest = (lang, topic, topicLink) => {
- const postData = {
+ const payload = {
"topic": topic,
"is_new": false,
'new_ref': topicLink.ref,
'interface_lang': lang === 'en' ? 'english' : 'hebrew',
'description' : {...topicLink.descriptions[lang], 'review_state': 'reviewed'}
};
- return Sefaria.postToApi(`/api/ref-topic-links/${topicLink.ref}`, {}, postData);
+ return Sefaria.postRefTopicLink(topicLink.ref, payload);
}
const useReviewState = (topic, topicLink) => {
diff --git a/static/js/TopicEditor.jsx b/static/js/TopicEditor.jsx
index a916a71685..9680617df1 100644
--- a/static/js/TopicEditor.jsx
+++ b/static/js/TopicEditor.jsx
@@ -1,5 +1,5 @@
import Sefaria from "./sefaria/sefaria";
-import {InterfaceText, requestWithCallBack, TopicPictureUploader} from "./Misc";
+import {InterfaceText, TopicPictureUploader} from "./Misc";
import $ from "./sefaria/sefariaJquery";
import {AdminEditor} from "./AdminEditor";
import {Reorder} from "./CategoryEditor";
@@ -35,6 +35,8 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => {
const [isChanged, setIsChanged] = useState(false);
const [changedPicture, setChangedPicture] = useState(false);
+ const disambiguationExtractionRegex = /\((.+)\)$/;
+
const toggle = function() {
setSavingStatus(savingStatus => !savingStatus);
}
@@ -93,12 +95,12 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => {
alert(Sefaria._("Title must be provided."));
return false;
}
- if (data.enImgCaption.length > 150) {
- alert("English caption is too long. It should not be more than 150 characters");
+ if (data.enImgCaption.length > 300) {
+ alert("English caption is too long. It should not be more than 300 characters");
return false;
}
- if (data.heImgCaption.length > 150) {
- alert("Hebrew caption is too long. It should not be more than 150 characters")
+ if (data.heImgCaption.length > 300) {
+ alert("Hebrew caption is too long. It should not be more than 300 characters")
return false;
}
if (sortedSubtopics.length > 0 && !isNew) {
@@ -109,14 +111,44 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => {
const saveReorderedSubtopics = function () {
const url = `/api/topic/reorder`;
const postCategoryData = {topics: sortedSubtopics};
- requestWithCallBack({url, data: postCategoryData, setSavingStatus, redirect: () => window.location.href = "/topics"});
+ Sefaria.adminEditorApiRequest(url, null, postCategoryData)
+ .then(() => window.location.href = "/topics")
+ .finally(() => setSavingStatus(false));
}
+ const extractDisambiguationFromTitle = function(titleText){
+ return titleText.match(disambiguationExtractionRegex)?.[1];
+ }
+ const removeDisambiguationFromTitle = function(titleText){
+ return titleText.replace(disambiguationExtractionRegex, "").trimEnd();
+ }
+
+ const createPrimaryTitleObj = function(rawTitle, lang){
+ let primaryTitleObj = {'text': removeDisambiguationFromTitle(rawTitle), "lang": lang, "primary": true};
+ let disambiguation = extractDisambiguationFromTitle(rawTitle);
+ if (disambiguation) {primaryTitleObj["disambiguation"]=disambiguation};
+ return primaryTitleObj;
+ };
+ const createNonPrimaryTitleObjArray = function(altTitles, lang){
+ const titleObjArray = []
+ altTitles.forEach((title) => {
+ let titleObj = {'text': removeDisambiguationFromTitle(title), "lang": lang};
+ let disambiguation = extractDisambiguationFromTitle(title);
+ if (disambiguation) {titleObj["disambiguation"]=disambiguation}
+ titleObjArray.push(titleObj)
+ });
+ return titleObjArray
+ };
+
const prepData = () => {
// always add category, title, heTitle, altTitles
- let postData = { category: data.catSlug, title: data.enTitle, heTitle: data.heTitle, altTitles: {}};
- postData.altTitles.en = data.enAltTitles.map(x => x.name); // alt titles implemented using TitleVariants which contains list of objects with 'name' property.
- postData.altTitles.he = data.heAltTitles.map(x => x.name);
+ let postData = { category: data.catSlug, titles: []};
+
+ //convert title and altTitles to the database format, including extraction of disambiguation from title string
+ postData['titles'].push(createPrimaryTitleObj(data.enTitle, 'en'));
+ postData['titles'].push(createPrimaryTitleObj(data.heTitle, 'he'));
+ postData['titles'] = postData['titles'].concat(createNonPrimaryTitleObjArray(data.enAltTitles.map(x => x.name), 'en'));
+ postData['titles'] = postData['titles'].concat(createNonPrimaryTitleObjArray(data.heAltTitles.map(x => x.name), 'he'));
// add image if image or caption changed
const origImageURI = origData?.origImage?.image_uri || "";
@@ -189,7 +221,7 @@ const TopicEditor = ({origData, onCreateSuccess, close, origWasCat}) => {
const deleteObj = function() {
const url = `/api/topic/delete/${data.origSlug}`;
- requestWithCallBack({url, type: "DELETE", redirect: () => window.location.href = "/topics"});
+ Sefaria.adminEditorApiRequest(url, null, null, "DELETE").then(() => window.location.href = "/topics");
}
let items = ["Title", "Hebrew Title", "English Description", "Hebrew Description", "Category Menu", "English Alternate Titles", "Hebrew Alternate Titles",];
if (isCategory) {
diff --git a/static/js/TopicPage.jsx b/static/js/TopicPage.jsx
index 51686b1a37..0d25a0dac8 100644
--- a/static/js/TopicPage.jsx
+++ b/static/js/TopicPage.jsx
@@ -104,8 +104,6 @@ const refSort = (currSortOption, a, b) => {
return a.order.comp_date - b.order.comp_date;
}
else {
- const aAvailLangs = a.order.availableLangs;
- const bAvailLangs = b.order.availableLangs;
if ((Sefaria.interfaceLang === 'english') &&
(a.order.curatedPrimacy.en > 0 || b.order.curatedPrimacy.en > 0)) {
return b.order.curatedPrimacy.en - a.order.curatedPrimacy.en; }
@@ -113,12 +111,13 @@ const refSort = (currSortOption, a, b) => {
(a.order.curatedPrimacy.he > 0 || b.order.curatedPrimacy.he > 0)) {
return b.order.curatedPrimacy.he - a.order.curatedPrimacy.he;
}
+ const aAvailLangs = a.order.availableLangs;
+ const bAvailLangs = b.order.availableLangs;
if (Sefaria.interfaceLang === 'english' && aAvailLangs.length !== bAvailLangs.length) {
if (aAvailLangs.indexOf('en') > -1) { return -1; }
if (bAvailLangs.indexOf('en') > -1) { return 1; }
return 0;
}
- else if (a.order.custom_order !== b.order.custom_order) { return b.order.custom_order - a.order.custom_order; } // custom_order, when present, should trump other data
else if (a.order.pr !== b.order.pr) {
return b.order.pr - a.order.pr;
}
@@ -345,7 +344,7 @@ const generatePrompts = async(topicSlug, linksToGenerate) => {
});
const payload = {ref_topic_links: linksToGenerate};
try {
- await Sefaria.postToApi(`/api/topics/generate-prompts/${topicSlug}`, {}, payload);
+ await Sefaria.apiRequestWithBody(`/api/topics/generate-prompts/${topicSlug}`, {}, payload);
const refValues = linksToGenerate.map(item => item.ref).join(", ");
alert("The following prompts are generating: " + refValues);
} catch (error) {
@@ -360,7 +359,7 @@ const publishPrompts = async (topicSlug, linksToPublish) => {
ref.descriptions[lang]["published"] = true;
});
try {
- const response = await Sefaria.postToApi(`/api/ref-topic-links/bulk`, {}, linksToPublish);
+ const response = await Sefaria.apiRequestWithBody(`/api/ref-topic-links/bulk`, {}, linksToPublish);
const refValues = response.map(item => item.anchorRef).join(", ");
const shouldRefresh = confirm("The following prompts have been published: " + refValues + ". Refresh page to see results?");
if (shouldRefresh) {
diff --git a/static/js/TopicSearch.jsx b/static/js/TopicSearch.jsx
index c736cc72cf..6f4578805d 100644
--- a/static/js/TopicSearch.jsx
+++ b/static/js/TopicSearch.jsx
@@ -64,31 +64,25 @@ class TopicSearch extends Component {
}
post(slug) {
- const postJSON = JSON.stringify({"topic": slug, 'interface_lang': Sefaria.interfaceLang});
+ const postJSON = {"topic": slug, 'interface_lang': Sefaria.interfaceLang};
const srefs = this.props.srefs;
const update = this.props.update;
const reset = this.reset;
- $.post("/api/ref-topic-links/" + Sefaria.normRef(this.props.srefs), {"json": postJSON}, async function (data) {
- if (data.error) {
- alert(data.error);
- } else {
+ Sefaria.postRefTopicLink(Sefaria.normRef(this.props.srefs), postJSON).then(async () => {
const sectionRef = await Sefaria.getRef(Sefaria.normRef(srefs)).sectionRef;
srefs.map(sref => {
- if (!Sefaria._refTopicLinks[sref]) {
- Sefaria._refTopicLinks[sref] = [];
- }
- Sefaria._refTopicLinks[sref].push(data);
+ if (!Sefaria._refTopicLinks[sref]) {
+ Sefaria._refTopicLinks[sref] = [];
+ }
+ Sefaria._refTopicLinks[sref].push(data);
});
if (!Sefaria._refTopicLinks[sectionRef]) {
- Sefaria._refTopicLinks[sectionRef] = [];
+ Sefaria._refTopicLinks[sectionRef] = [];
}
Sefaria._refTopicLinks[sectionRef].push(data);
update();
reset();
alert("Topic added.");
- }
- }).fail(function (xhr, status, errorThrown) {
- alert("Unfortunately, there may have been an error saving this topic information: " + errorThrown);
});
}
diff --git a/static/js/context.js b/static/js/context.js
index 8c9f1f03f4..9b9d0778bf 100644
--- a/static/js/context.js
+++ b/static/js/context.js
@@ -19,7 +19,8 @@ function StrapiDataProvider({ children }) {
const [modal, setModal] = useState(null);
const [banner, setBanner] = useState(null);
useEffect(() => {
- if (STRAPI_INSTANCE) {
+ // Disable Strapi API calls during Unbounce trial
+ if (false && typeof STRAPI_INSTANCE !== "undefined" && STRAPI_INSTANCE) {
const getStrapiData = async () => {
let getDateWithoutTime = (date) => date.toISOString().split("T")[0];
let getJSONDateStringInLocalTimeZone = (date) => {
diff --git a/static/js/lib/keyboard.js b/static/js/lib/keyboard.js
index c90c35f00c..0f3cc4353a 100644
--- a/static/js/lib/keyboard.js
+++ b/static/js/lib/keyboard.js
@@ -1486,6 +1486,8 @@ var VKI_attach, VKI_close;
break;
case "Enter":
VKI_addListener(td, 'click', function() {
+ let element = document.querySelector('[vki_attached="true"]');
+ element.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true}));
if (self.VKI_target.nodeName != "TEXTAREA") {
if (self.VKI_enterSubmit && self.VKI_target.form) {
for (var z = 0, subm = false; z < self.VKI_target.form.elements.length; z++)
diff --git a/static/js/linker.v3/main.js b/static/js/linker.v3/main.js
index b5dab6b852..700de691c4 100644
--- a/static/js/linker.v3/main.js
+++ b/static/js/linker.v3/main.js
@@ -6,6 +6,10 @@ import {LinkExcluder} from "./excluder";
(function(ns) {
+ function escapeRegExp(string) {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+ }
+
function sanitizeElem(elem) {
const cleaned = DOMPurify.sanitize(elem, { USE_PROFILES: { html: true } });
const cleanedElem = document.createElement("div");
@@ -100,7 +104,6 @@ import {LinkExcluder} from "./excluder";
function findOccurrences(text) {
const occurrences = [];
findAndReplaceDOMText(document, {
- preset: 'prose',
find: text,
replace: function(portion, match) {
if (portion.index === 0) {
@@ -114,7 +117,7 @@ import {LinkExcluder} from "./excluder";
function getNextWhiteSpaceIndex(text) {
const match = text.match(/\S\s+/); // `\S` so whitespace can't be at beginning of string
- if (match === null || text.substring(0, match.index+1).indexOf('\n') > -1) { return -1; } // \n's are added in by Readability and therefore make it challenging to match against. stop when you hit one.
+ if (match === null) { return -1; }
return match.index + 1;
}
@@ -142,9 +145,12 @@ import {LinkExcluder} from "./excluder";
const newEndChar = getNthWhiteSpaceIndex(text, numWordsAround, endChar);
const textRev = [...text].reverse().join("");
const newStartChar = text.length - getNthWhiteSpaceIndex(textRev, numWordsAround, text.length - startChar);
- const wordsAroundText = text.substring(newStartChar, newEndChar);
+ const wordsAroundText = escapeRegExp(text.substring(newStartChar, newEndChar));
+ // findAndReplaceDOMText and Readability deal with element boundaries differently
+ // in order to more flexibly find these boundaries, we treat all whitespace the same
+ const wordsAroundReg = wordsAroundText.replace(/\s+/g, '\\s+');
return {
- text: wordsAroundText,
+ text: RegExp(wordsAroundReg, "g"),
startChar: startChar - newStartChar,
};
}
@@ -193,6 +199,26 @@ import {LinkExcluder} from "./excluder";
return node;
}
}
+ function isMatchUniqueEnough(globalLinkStarts, match, charError=5) {
+ /**
+ * Return true if `match` represents one of the matches we've determined to be unique enough to represent this link
+ */
+ for (let globalStart of globalLinkStarts) {
+ if (Math.abs(match.startIndex - globalStart) <= charError) {
+ return true;
+ }
+ }
+ return false;
+ }
+ function isMatchedTextUniqueEnough(occurrences, linkObj, maxSearchLength=30) {
+ /**
+ * return true if first occurrence is sufficiently long (longer than `maxSearchLength`)
+ * AND searchText includes more than just the text of the link.
+ */
+ if (occurrences.length === 0) { return false; }
+ const firstOccurrenceLength = occurrences[0][1] - occurrences[0][0];
+ return firstOccurrenceLength >= maxSearchLength && firstOccurrenceLength > linkObj.text.length;
+ }
function wrapRef(linkObj, normalizedText, refData, iLinkObj, resultsKey, maxNumWordsAround = 10, maxSearchLength = 30) {
/**
@@ -218,9 +244,9 @@ import {LinkExcluder} from "./excluder";
({ text: searchText, startChar: linkStartChar } = getNumWordsAround(linkObj, normalizedText, numWordsAround));
occurrences = findOccurrences(searchText);
numWordsAround += 1;
- if (searchText.length >= maxSearchLength) { break; }
+ if (isMatchedTextUniqueEnough(occurrences, linkObj, maxSearchLength)) { break; }
}
- if (occurrences.length === 0 || (occurrences.length > 1 && searchText.length < maxSearchLength)) {
+ if (occurrences.length !== 1 && !isMatchedTextUniqueEnough(occurrences, linkObj, maxSearchLength)) {
if (ns.debug) {
console.log("MISSED", numWordsAround, occurrences.length, linkObj);
}
@@ -228,11 +254,11 @@ import {LinkExcluder} from "./excluder";
}
const globalLinkStarts = occurrences.map(([start, end]) => linkStartChar + start);
findAndReplaceDOMText(document, {
- preset: 'prose',
find: linkObj.text,
replace: function(portion, match) {
- // check this is the unique match found above
- if (globalLinkStarts.indexOf(match.startIndex) === -1) { return portion.text; }
+ if (!isMatchUniqueEnough(globalLinkStarts, match)) {
+ return portion.text;
+ }
// check if should be excluded from linking and/or tracking
const matchKey = match.startIndex + "|" + match.endIndex;
diff --git a/static/js/linker.v3/popup.js b/static/js/linker.v3/popup.js
index fe80d935cb..aaeeb01614 100644
--- a/static/js/linker.v3/popup.js
+++ b/static/js/linker.v3/popup.js
@@ -289,7 +289,7 @@ export class PopupManager {
this.linkerHeader.style["border-top-color"] = this.category_colors[primaryCategory];
// TODO is this right?
- if (this.contentLang !== "he") {
+ if (this.contentLang.slice(0, 2) !== "he") {
// [].forEach.call(heElems, function(e) {e.style.display = "None"});
this.heTitle.style.display = "None";
[].forEach.call(this.enElems, function(e) {e.style.display = "Block"});
@@ -413,6 +413,9 @@ export class PopupManager {
elem.addEventListener('mouseout', this.hidePopup, false);
} else if (this.mode === "popup-click") {
elem.addEventListener('click', (event) => {
+ if (event.ctrlKey) {
+ return;
+ }
event.preventDefault();
event.stopPropagation();
this.showPopup(elem, source);
@@ -420,4 +423,4 @@ export class PopupManager {
}, false);
}
}
-}
\ No newline at end of file
+}
diff --git a/static/js/sefaria/sefaria.js b/static/js/sefaria/sefaria.js
index ecffc50cd5..c9b99d7673 100644
--- a/static/js/sefaria/sefaria.js
+++ b/static/js/sefaria/sefaria.js
@@ -479,16 +479,16 @@ Sefaria = extend(Sefaria, {
let refStrs = [""];
refs.map(ref => {
let last = refStrs[refStrs.length-1];
- const encodedRef = encodeURIComponent(ref)
- if (`${hostStr}${last}|${encodedRef}${paramStr}`.length > MAX_URL_LENGTH) {
- refStrs.push(encodedRef)
+ const encodedFullURL = encodeURI(`${hostStr}${last}|${ref}${paramStr}`);
+ if (encodedFullURL.length > MAX_URL_LENGTH) {
+ refStrs.push(ref)
} else {
- refStrs[refStrs.length-1] += last.length ? `|${encodedRef}` : encodedRef;
+ refStrs[refStrs.length-1] += last.length ? `|${ref}` : ref;
}
});
let promises = refStrs.map(refStr => this._cachedApiPromise({
- url: `${hostStr}${refStr}${paramStr}`,
+ url: `${hostStr}${encodeURIComponent(refStr)}${paramStr}`,
key: refStr + paramStr,
store: this._bulkTexts
}));
@@ -623,27 +623,52 @@ Sefaria = extend(Sefaria, {
firstName: firstName,
lastName: lastName,
};
- return await Sefaria.postToApi(`/api/subscribe/${email}`, null, payload);
+ return await Sefaria.apiRequestWithBody(`/api/subscribe/${email}`, null, payload);
},
subscribeSteinsaltzNewsletter: async function(firstName, lastName, email) {
const payload = {firstName, lastName};
- return await Sefaria.postToApi(`/api/subscribe/steinsaltz/${email}`, null, payload);
+ return await Sefaria.apiRequestWithBody(`/api/subscribe/steinsaltz/${email}`, null, payload);
},
-
- postToApi: async function(url, urlParams, payload) {
+ postRefTopicLink: function(refInUrl, payload) {
+ const url = `/api/ref-topic-links/${Sefaria.normRef(refInUrl)}`;
+ // payload will need to be refactored once /api/ref-topic-links takes a more standard input
+ return Sefaria.adminEditorApiRequest(url, null, payload);
+ },
+ adminEditorApiRequest: async function(url, urlParams, payload, method="POST") {
+ /**
+ * Wraps apiRequestWithBody() with basic alerting if response has an error
+ */
+ let result;
+ try {
+ result = await Sefaria.apiRequestWithBody(url, urlParams, payload, method);
+ } catch (e) {
+ alert(Sefaria._("Something went wrong. Sorry!"));
+ throw e;
+ }
+ if (result.error) {
+ alert(result.error);
+ throw result.error;
+ } else {
+ return result;
+ }
+ },
+ apiRequestWithBody: async function(url, urlParams, payload, method="POST") {
+ /**
+ * Generic function for performing an API request with a payload. Payload and urlParams are optional and will not be used if falsy.
+ */
let apiUrl = this.apiHost + url;
if (urlParams) {
apiUrl += '?' + new URLSearchParams(urlParams).toString();
}
const response = await fetch(apiUrl, {
- method: "POST",
+ method,
mode: 'same-origin',
headers: {
'X-CSRFToken': Cookies.get('csrftoken'),
'Content-Type': 'application/json'
},
credentials: 'same-origin',
- body: JSON.stringify(payload)
+ body: payload && JSON.stringify(payload)
});
if (!response.ok) {
diff --git a/static/js/sefaria/strings.js b/static/js/sefaria/strings.js
index c913571cf2..752f95cdf1 100644
--- a/static/js/sefaria/strings.js
+++ b/static/js/sefaria/strings.js
@@ -282,6 +282,7 @@ const Strings = {
"Location: ": "מיקום: ",
"Translations": "תרגומים",
"Uncategorized": "לא מסווג",
+ "Text display options": "אפשרויות תצוגת טקסט",
// Collections
"Collections": "אסופות",
diff --git a/templates/base.html b/templates/base.html
index 13238c6d3e..9aba8e05ff 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -151,7 +151,8 @@
{% endif %}
-
+
+
diff --git a/templates/static/en/about.html b/templates/static/en/about.html
index 11cf8b0a87..f8ec4373fd 100644
--- a/templates/static/en/about.html
+++ b/templates/static/en/about.html
@@ -272,6 +272,18 @@
Sefaria adds a French Jerusalem Talmud to its collection of translated texts, which includes a German translation of the Babylonian Talmud.
+
+
+
+ Along with the rest of the Jewish world, the Sefaria team mourned the horrific attacks carried out on October 7th. In the difficult months that followed, Sefaria worked to support our colleagues in Israel as well as to continue expanding access to Torah as a source of comfort and strength for the Jewish people.
+
+
+ Sefaria’s R&D arm, Sefaria Ventures, launches a groundbreaking partnership with AppliedAI and the Technical University of Munich (TUM) to explore the possibilities of leveraging AI to significantly expand access to Torah.
+
+
+ Sefaria partners with the Steinsaltz Center and Aleph Society to launch a digital collection of Rabbi Adin Steinsaltz’s complete works of commentary, making the renowned Rabbi’s writings available to all who wish to learn.
+
+
diff --git a/templates/static/he/about.html b/templates/static/he/about.html
index a48e15d850..b3a7ee5736 100644
--- a/templates/static/he/about.html
+++ b/templates/static/he/about.html
@@ -279,6 +279,18 @@
ספריא מוסיפה את התלמוד הירושלמי בצרפתית לאוסף המקורות המתורגמים, הכולל תרגום של התלמוד הבבלי לגרמנית.
+
+
+
+ יחד עם כל העולם היהודי, צוות ספריא התאבל על הטבח הנורא של ה-7 באוקטובר. בחודשים הקשים שלאחר מכן פעלנו כדי לתמוך בעמיתינו בישראל וכדי להמשיך את הגדלת הספריה והנגישות למקורות היהדות שמהווים עבור רבים בעם היהודי מקור לנחמה, כוח ותקווה.
+
+
+ מחלקת המחקר והפיתוח של ספריא משיקה שותפות פורצת דרך עם AppliedAI והאוניברסיטה הטכנית של מינכן (TUM). מטרת השותפות היא בחינת האפשרויות הטמונות במינוף של אינטליגנציה מלאכותית ככלי להרחבה משמעותית של גישה ציבורית לתורה.
+
+
+ ספריא יוצרת שותפות עם מרכז שטיינזלץ וה-Aleph Society כדי להשיק אוסף דיגיטלי של כל הפרשנויות שכתב הרב עדין שטיינזלץ, ובכך להנגיש את כלל כתביו של הרב הנודע לכל לומד או לומדת באשר יהיו.
+
+