diff --git a/css/car-pictures.css b/css/car-pictures.css index 3c69bc1..fe57781 100644 --- a/css/car-pictures.css +++ b/css/car-pictures.css @@ -18,9 +18,9 @@ section { } .approval-btn { - margin-top: 29px; + /* margin-top: 29px; */ align-self: flex-start; - margin-left: 15px; + /* margin-left: 15px; */ } #button-and-posts-separating-line { @@ -29,13 +29,48 @@ section { color: azure; } +#aboveTopLineDiv { + display: flex; + width: 95%; + align-items: center; + justify-content: space-evenly; + margin-top: 16px; +} + +#aboveTopLineDiv > form { + width: 50%; + margin: 0 !important; +} + +#aboveTopLineDiv > form > input.form-control{ + width: 50%; + outline: none !important; + box-shadow: unset !important; +} + +#aboveTopLineDiv > form > .btn { + border-radius: 0px !important; +} + +#aboveTopLineDiv > form > .btn:last-child { + border-top-right-radius: 3px !important; + border-bottom-right-radius: 3px !important; +} + +/* TODO: Back4app remove post from Redis */ + +#aboveTopLineDiv > form > .btn-secondary{ + border-top-left-radius: 0px !important; + border-bottom-left-radius: 0px !important; +} + section { display: flex; flex-direction: column; flex-wrap: nowrap; align-items: center; - margin-top: 55px; + margin-top: 66px; padding-bottom: 2%; height: auto; min-height: 100vh; @@ -46,11 +81,17 @@ section { --card-margin-top: 30px; } +@media only screen and (min-width: 1440px) { + section { + margin-top: calc(4.4vw) !important; + } +} + #unapprovedPostsMessageContainer { width: var(--card-width); min-width: 250px; - margin-top: 34px; + /* margin-top: 34px; */ padding: 13px; border: 5px dashed darkkhaki; @@ -75,7 +116,7 @@ section { } .card { - margin-top: var(--card-margin-top) !important; + margin-top: 14px !important; width: var(--card-width) !important; min-width: 250px !important; border: none !important; @@ -1098,6 +1139,7 @@ ul.extra-comment-actions:focus-within { user-select: none; pointer-events: none; justify-self: center; + text-align: center; } .loader { @@ -1166,6 +1208,14 @@ ul.extra-comment-actions:focus-within { } } +#loader-wrapper { + width: 100%; + flex: 1; + display: flex; + justify-content: center; + align-items: center; +} + /* ? Custom scrollbar */ /* Firefox */ * { diff --git a/src/api/posts.js b/src/api/posts.js index 166cf7e..00398c6 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -9,7 +9,7 @@ export async function* get2PostObjects() { if (result.length == 0) { //? If we didn't get any posts from server, return an empty array and signal the end of this generator. - return []; + return []; } else { yield result; //? Otherwise, yield the result and wait for the next call. } @@ -31,6 +31,38 @@ export async function* get2UnapprovedPostObjects() { } } +export async function* get2SearchedPostObjects(searchedPostsIds) { + for (let i = 0; i < searchedPostsIds.length; i += 2) { + const result = (await api.post('/functions/get2PostsByIds', { searchedPostsIds: JSON.stringify(searchedPostsIds.slice(i, i + 2)) })).result; + + if (result.length == 0) { + //? If we didn't get any posts from server, return an empty array and signal the end of this generator. + return []; + } else if (result.length == 1) { + //? if we got the last post, signal the end of this generator. + return result; + } else { + yield result; //? Otherwise, yield the result and wait for the next call. + } + } +} + +export async function* get2UnapprovedSearchedPostObjects(searchedPostsIds) { + for (let i = 0; i < searchedPostsIds.length; i += 2) { + const result = (await api.post('/functions/get2UnapprovedPostsByIds', { searchedPostsIds: JSON.stringify(searchedPostsIds.slice(i, i + 2)) })).result; + + if (result.length == 0) { + //? If we didn't get any posts from server, return an empty array and signal the end of this generator. + return []; + } else if (result.length == 1) { + //? if we got the last post, signal the end of this generator. + return result; + } else { + yield result; //? Otherwise, yield the result and wait for the next call. + } + } +} + async function getUnapprovedPostsCount() { return (await api.post('/functions/getUnapprovedPostsCount')).result } @@ -74,8 +106,8 @@ export async function likePost(postId) { } export async function unlikePost(postId, userId) { - const { objectId: likeObjId } = - (await api.get(`/PostsLikes?where={"postId": "${postId}", + const { objectId: likeObjId } = + (await api.get(`/PostsLikes?where={"postId": "${postId}", "userWhoLiked": {"__type":"Pointer","className":"_User","objectId":"${userId}"}}`)).results[0]; return api.del(`/PostsLikes/${likeObjId}`); } @@ -83,4 +115,28 @@ export async function unlikePost(postId, userId) { export async function reportObject(report) { addEntryWithUserPointer(report, 'reporter'); await api.post('/functions/reportObject', report); +} + +export async function searchForPosts(srchText) { + try { + const response = await fetch("https://ex0tic-cars.netlify.app/.netlify/functions/search", + { body: srchText, method: 'POST' }); + + if (!response.ok) { + const error = await response.json(); + const err = new Error(error.error); + err.code = error.code; + throw err; + } + + if (response.status == 204) { + return []; + } else { + const srchResultsArr = await response.json(); + return srchResultsArr; + } + } catch (error) { + alert(error.message); + throw error; + } } \ No newline at end of file diff --git a/src/app.js b/src/app.js index 66c8f80..2edb532 100644 --- a/src/app.js +++ b/src/app.js @@ -13,6 +13,13 @@ import { sharePhotosView } from "./views/share-photos.js"; import { showroomsView } from "./views/showrooms.js"; import { verifyView } from "./views/verify.js"; +sessionStorage.removeItem('popstateChanges'); + +window.addEventListener('popstate', () => { + let popstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + sessionStorage.setItem('popstateChanges', ++popstateChanges); +}); + const main = document.querySelector('#main'); const root = main.attachShadow({ mode: 'open' }); @@ -32,14 +39,18 @@ page((ctx, next) => { page('/index.html', '/'); page('/', homeView); page('/car-pictures', carPicturesView); +page.exit('/car-pictures', (ctx, next) => { + ctx.carPicturesController?.abort(); + next(); +}); page('/about-us', aboutUsView); page('/showrooms', showroomsView); page('/share-photos', sharePhotosView); page('/share-photos:id', editPostView); -page.exit('/share-photos', exitFunction); -page.exit('/share-photos:id', exitFunction); -function exitFunction(ctx, next) { +page.exit('/share-photos', sharePhotosExitFunction); +page.exit('/share-photos:id', sharePhotosExitFunction); +function sharePhotosExitFunction(ctx, next) { ctx.controller.abort(); //? remove navbar's listener for click event after user leaves "share-photos" sessionStorage.removeItem('userHasUnsavedData'); next(); diff --git a/src/views/car-pictures/car-pictures.js b/src/views/car-pictures/car-pictures.js index 81a0a83..ebbd9bb 100644 --- a/src/views/car-pictures/car-pictures.js +++ b/src/views/car-pictures/car-pictures.js @@ -1,89 +1,42 @@ -import { html } from "../../lib/lit-html.js"; -import { until } from "../../lib/directives/until.js"; -import { get2PostObjects, get2UnapprovedPostObjects } from "../../api/posts.js"; -import { sectionClickHandler } from "./infoWindow/infoWindow.js"; -import { carPicturesTemplate } from "./carPicturesTemplate.js"; -import { getSwitchToApprovalModeHandler } from "./posts/switchToApprovalModeHandler.js"; -import { getNoPostsTemplate } from "./posts/getNoPostsTemplate.js"; -import { getUnapprovedPostsMessageTemplate } from "./posts/unapprovedPostsMessageTemplate.js"; -import { getSectionContentTemplate } from "./posts/sectionContentTemplate.js"; -import { setUpMiscStuff } from "./posts/setUpMiscStuff.js"; -import { startObservingTheThirdLastCard } from "./posts/startObservingTheThirdLastCard.js"; +import { renderNew, resetState } from "./renderNew.js"; export async function carPicturesView(ctx) { - const loadingTemplate = () => html` -

Loading posts
-

- ` - const posts = []; - const generatorsObject = { - asyncPostsGenerator: get2PostObjects(), - asyncPostsGeneratorIsDone: false, - asyncUnapprovedPostsGenerator: get2UnapprovedPostObjects(), - asyncUnapprovedPostsGeneratorIsDone: false, + if (!ctx.user?.isModerator && ctx.hash == 'approval-mode') { + ctx.page.redirect('/car-pictures'); + return; } - let miscStuffSetUp = false; - let portionsRendered = 0; - - async function renderNew() { - if (generatorsObject.asyncPostsGeneratorIsDone - || generatorsObject.asyncUnapprovedPostsGeneratorIsDone) { - //? If one of the generators is exhausted, don't bother rendering anything. - return; - } - const sectionContentPromise = getSectionContentTemplate( - getNoPostsTemplate, - ctx.hash == 'approval-mode' ? 'unapproved' : null, - posts, - generatorsObject, - ctx, - ); + const posts = []; + resetState(posts); - if (posts.length > 0) { - //? If we already have 2 posts, the displaying of the "loading" template is avoided and the posts are shown after they have loaded. - await sectionContentPromise; - } + ctx.carPicturesController = new AbortController(); + window.addEventListener('popstate', () => { + resetState(posts, ctx.user?.isModerator); - /* - ? After awaiting the sectionContentPromise and resuming the execution of this async function, - ? we check if the page's hash is different from the hash in the "ctx" object - ? (the post mode has been switched to something else - ? while the sectionContentPromise was still in "pending" state). - ? If this is the case we won't bother rendering anything into the section. - */ - if (window.location.hash.slice(1) !== ctx.hash) { - return; - } + const srchRegex = /(?<=search=)(?.*?)(?=&|$)/m; - ctx.render( - carPicturesTemplate( - ev => sectionClickHandler(ev, posts, ctx), - until(sectionContentPromise, loadingTemplate()), - ctx.user?.isModerator ? getSwitchToApprovalModeHandler(ctx) : null, - ctx.user ? until(getUnapprovedPostsMessageTemplate(sectionContentPromise), null) : null, - ctx, - ) - ); + const queryStr = window.location.search.slice(1); - if (!miscStuffSetUp) { - setUpMiscStuff(ctx, sectionContentPromise); - miscStuffSetUp = true; + let urlSrchText = null; //? this text will be used in renderNew for auto search when first rendering + if (queryStr) { + urlSrchText = srchRegex.exec(decodeURIComponent(queryStr)).groups.srchStr.trim(); } - await sectionContentPromise; //? we wait for the two cards to show up on the screen + //? (in getSectionContentTemplate) the window path may change while some post promises are still pending so we create this variable to prevent the rendering of posts in a wrong view + let windowPath = window.location.href.replace(window.location.origin, ''); + windowPath = window.location.hash ? windowPath : windowPath.replace("#", ''); + + renderNew(ctx, posts, urlSrchText ? [] : null, urlSrchText || null, windowPath); + }, { signal: ctx.carPicturesController.signal }); - portionsRendered++; + if (posts.length == 0) { //? If there are no posts, start rendering them. + const srchRegex = /(?<=search=)(?.*?)(?=&|$)/m; - if (portionsRendered < 2) { - renderNew(); //? render one more portion - } else { - portionsRendered = 0; - startObservingTheThirdLastCard(ctx, renderNew); //? When user sees the third card, repeat the rendering cycle. + let urlSrchText = null; //? this text will be used in renderNew for auto search when first rendering + if (ctx.querystring) { + urlSrchText = srchRegex.exec(decodeURIComponent(ctx.querystring)).groups.srchStr.trim(); } - } - if (posts.length == 0) { //? If there are no posts, start rendering them. - renderNew(); + renderNew(ctx, posts, urlSrchText ? [] : null, urlSrchText || null); } } \ No newline at end of file diff --git a/src/views/car-pictures/carPicturesTemplate.js b/src/views/car-pictures/carPicturesTemplate.js index c6e9386..b27e01a 100644 --- a/src/views/car-pictures/carPicturesTemplate.js +++ b/src/views/car-pictures/carPicturesTemplate.js @@ -1,6 +1,6 @@ import { html } from "../../lib/lit-html.js"; -export const carPicturesTemplate = (sectionClickHandler, postsTemplate, onSwitchToApprovalMode, unapprovedPostsMessageTemplatePromise, ctx) => html` +export const carPicturesTemplate = (sectionClickHandler, postsTemplate, onSwitchToApprovalMode, unapprovedPostsMessageTemplatePromise, searchHandler, ctx, searchMode) => html` @@ -8,13 +8,25 @@ export const carPicturesTemplate = (sectionClickHandler, postsTemplate, onSwitch
- ${ - onSwitchToApprovalMode ? - html` - -
- ` : null - } +
+ ${ + onSwitchToApprovalMode ? + html` + + ` : null + } +
+ + + ${ + searchMode ? + html`Go back` + : null + } + +
+
+
${unapprovedPostsMessageTemplatePromise} ${postsTemplate}
diff --git a/src/views/car-pictures/checkIfRenderShouldStop.js b/src/views/car-pictures/checkIfRenderShouldStop.js new file mode 100644 index 0000000..f75a8df --- /dev/null +++ b/src/views/car-pictures/checkIfRenderShouldStop.js @@ -0,0 +1,13 @@ +export function checkIfRenderShouldStop(arrOfPostObjectIds, generatorsObject, ctx) { + if (arrOfPostObjectIds?.length > 0 && (generatorsObject.asyncSearchPostsGeneratorIsDone || generatorsObject.asyncUnapprovedSearchPostsGeneratorIsDone)) { + //? If one of the generators is exhausted, don't bother rendering anything. + return true; + } + + if ((arrOfPostObjectIds === undefined || arrOfPostObjectIds === null) && ((generatorsObject.asyncPostsGeneratorIsDone && ctx.hash != 'approval-mode') || (generatorsObject.asyncUnapprovedPostsGeneratorIsDone && ctx.hash == 'approval-mode'))) { + //? If one of the generators is exhausted, don't bother rendering anything. + return true + } + + return false; +} \ No newline at end of file diff --git a/src/views/car-pictures/generatorsObject.js b/src/views/car-pictures/generatorsObject.js new file mode 100644 index 0000000..5311330 --- /dev/null +++ b/src/views/car-pictures/generatorsObject.js @@ -0,0 +1,22 @@ +import { get2PostObjects, get2UnapprovedPostObjects } from "../../api/posts.js"; + +export const generatorsObject = { + asyncPostsGenerator: get2PostObjects(), + asyncPostsGeneratorIsDone: false, + asyncUnapprovedPostsGenerator: get2UnapprovedPostObjects(), + asyncUnapprovedPostsGeneratorIsDone: false, + asyncSearchPostsGenerator: null, + asyncSearchPostsGeneratorIsDone: false, + asyncUnapprovedSearchPostsGenerator: null, + asyncUnapprovedSearchPostsGeneratorIsDone: false, + reset() { + this.asyncPostsGenerator = get2PostObjects(); + this.asyncPostsGeneratorIsDone = false; + this.asyncUnapprovedPostsGenerator = get2UnapprovedPostObjects(); + this.asyncUnapprovedPostsGeneratorIsDone = false; + this.asyncSearchPostsGenerator = null; + this.asyncSearchPostsGeneratorIsDone = false; + this.asyncUnapprovedSearchPostsGenerator = null; + this.asyncUnapprovedSearchPostsGeneratorIsDone = false; + }, +}; \ No newline at end of file diff --git a/src/views/car-pictures/posts/getNoPostsTemplate.js b/src/views/car-pictures/posts/getNoPostsTemplate.js index 7972b07..dd8f708 100644 --- a/src/views/car-pictures/posts/getNoPostsTemplate.js +++ b/src/views/car-pictures/posts/getNoPostsTemplate.js @@ -1,11 +1,15 @@ import { html } from "/src/lib/lit-html.js" -export const getNoPostsTemplate = (shouldIncludeCreateLink, ctx) => { - return ctx.user?.sessionToken ? - html` -

There are no posts yet. ${shouldIncludeCreateLink ? html`
ctx.page.redirect('/share-photos')} href="/share-photos">Create one!` : null}

- ` : - html` -

There are no posts yet.
ctx.page.redirect('/login')} href="/login">Log in and create one!

- ` +export const getNoPostsTemplate = (shouldIncludeCreateLink, ctx, nothingWasFound) => { + if (nothingWasFound) { + return html`

Nothing was found!

` + } else { + return ctx.user?.sessionToken ? + html` +

There are no posts yet. ${shouldIncludeCreateLink ? html`
ctx.page.redirect('/share-photos')} href="/share-photos">Create one!` : null}

+ ` : + html` +

There are no posts yet.
ctx.page.redirect('/login')} href="/login">Log in and create one!

+ ` + } } \ No newline at end of file diff --git a/src/views/car-pictures/posts/populatePostsArr.js b/src/views/car-pictures/posts/populatePostsArr.js new file mode 100644 index 0000000..2fb5bd9 --- /dev/null +++ b/src/views/car-pictures/posts/populatePostsArr.js @@ -0,0 +1,84 @@ +import { html } from "/src/lib/lit-html.js"; +import { getNoPostsTemplate } from './getNoPostsTemplate.js' + +export async function populatePostsArr (ctx, posts, generatorsObject, postsType, initialPopstateChanges, prevWindowPath, arrOfPostObjectIdsHasItems) { + if (postsType == 'unapproved') { + if (arrOfPostObjectIdsHasItems) { + const generatorReturnedObject = await generatorsObject.asyncUnapprovedSearchPostsGenerator.next(); + + generatorsObject.asyncUnapprovedSearchPostsGeneratorIsDone = generatorReturnedObject.done; + const currPopstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + + let curWindowPath = window.location.href.replace(window.location.origin, ''); + curWindowPath = window.location.hash ? curWindowPath : curWindowPath.replace("#", ''); + + if (generatorReturnedObject.value?.length > 0) { + const twoPosts = generatorReturnedObject.value; + + if ((currPopstateChanges > initialPopstateChanges) || ((curWindowPath !== prevWindowPath) && prevWindowPath.startsWith('/car-pictures'))) { + return Promise.reject("view changed"); + } + + posts.push(...twoPosts); + } else { + return html`${getNoPostsTemplate(postsType !== 'unapproved', ctx, true)}` + } + } else { + const generatorReturnedObject = await generatorsObject.asyncUnapprovedPostsGenerator.next(); + + generatorsObject.asyncUnapprovedPostsGeneratorIsDone = generatorReturnedObject.done; + const currPopstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + + let curWindowPath = window.location.href.replace(window.location.origin, ''); + curWindowPath = window.location.hash ? curWindowPath : curWindowPath.replace("#", ''); + + if (generatorReturnedObject.value?.length > 0) { + const twoPosts = generatorReturnedObject.value; + + if ((currPopstateChanges > initialPopstateChanges) || ((curWindowPath !== prevWindowPath) && prevWindowPath.startsWith('/car-pictures'))) { + return Promise.reject("view changed"); + } + + posts.push(...twoPosts); + } + } + } else { + if (arrOfPostObjectIdsHasItems) { + const generatorReturnedObject = await generatorsObject.asyncSearchPostsGenerator.next(); + generatorsObject.asyncSearchPostsGeneratorIsDone = generatorReturnedObject.done; + const currPopstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + + let curWindowPath = window.location.href.replace(window.location.origin, ''); + curWindowPath = window.location.hash ? curWindowPath : curWindowPath.replace("#", ''); + + if (generatorReturnedObject.value?.length > 0) { + const twoPosts = generatorReturnedObject.value; + + if ((currPopstateChanges > initialPopstateChanges) || ((curWindowPath !== prevWindowPath) && prevWindowPath.startsWith('/car-pictures'))) { + return Promise.reject("view changed"); + } + + posts.push(...twoPosts); + } else { + return html`${getNoPostsTemplate(postsType !== 'unapproved', ctx, true)}` + } + } else { + const generatorReturnedObject = await generatorsObject.asyncPostsGenerator.next(); + generatorsObject.asyncPostsGeneratorIsDone = generatorReturnedObject.done; + const currPopstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + + let curWindowPath = window.location.href.replace(window.location.origin, ''); + curWindowPath = window.location.hash ? curWindowPath : curWindowPath.replace("#", ''); + + if (generatorReturnedObject.value?.length > 0) { + const twoPosts = generatorReturnedObject.value; + + if ((currPopstateChanges > initialPopstateChanges) || ((curWindowPath !== prevWindowPath) && prevWindowPath.startsWith('/car-pictures'))) { + return Promise.reject("view changed"); + } + + posts.push(...twoPosts); + } + } + } +} \ No newline at end of file diff --git a/src/views/car-pictures/posts/sectionContentTemplate.js b/src/views/car-pictures/posts/sectionContentTemplate.js index 4bd34f5..b8dc8e9 100644 --- a/src/views/car-pictures/posts/sectionContentTemplate.js +++ b/src/views/car-pictures/posts/sectionContentTemplate.js @@ -2,32 +2,24 @@ import { html } from "../../../lib/lit-html.js"; import { repeat } from "../../../lib/directives/repeat.js"; import { cardTemplate } from "./cardTemplate.js"; import { getApproveClickHandler, getDeleteHandler, getLikeClickHandler } from "./postActions.js"; +import { populatePostsArr } from "./populatePostsArr.js"; -export async function getSectionContentTemplate (getNoPostsTemplate, postsType, posts, generatorsObject, ctx) { - try { - if (postsType == 'unapproved') { - const generatorReturnedObject = await generatorsObject.asyncUnapprovedPostsGenerator.next(); - generatorsObject.asyncUnapprovedPostsGeneratorIsDone = generatorReturnedObject.done; +export async function getSectionContentTemplate (getNoPostsTemplate, postsType, posts, generatorsObject, ctx, arrOfPostObjectIds, pageUrlSrchTextPromise, windowPath) { + //? the popstate may change while some post promises are still pending so we create this variable to prevent the rendering of posts in a wrong view + let initialPopstateChanges = Number(sessionStorage.getItem('popstateChanges') || 0); + + //? the window path may change while some post promises are still pending so we create this variable to prevent the rendering of posts in a wrong view + const prevWindowPath = windowPath || ctx.canonicalPath; - if (generatorReturnedObject.value) { - const twoPosts = generatorReturnedObject.value; - for (const post of twoPosts) { - posts.push(post); - } - } - } else { - const generatorReturnedObject = await generatorsObject.asyncPostsGenerator.next(); - generatorsObject.asyncPostsGeneratorIsDone = generatorReturnedObject.done; + if (pageUrlSrchTextPromise) { + await pageUrlSrchTextPromise; //? we wait for the searching of post object ids and the initialization of srch. gens. to finish + } - if (generatorReturnedObject.value) { - const twoPosts = generatorReturnedObject.value; - for (const post of twoPosts) { - posts.push(post); - } - } - } + try { + //? The following function may return a rejected promise, if views changed while some post promises were still pending. + await populatePostsArr(ctx, posts, generatorsObject, postsType, initialPopstateChanges, prevWindowPath, arrOfPostObjectIds?.length > 0); } catch (error) { - alert(error.message); + console.error(error); throw error; } diff --git a/src/views/car-pictures/posts/setUpMiscStuff.js b/src/views/car-pictures/posts/setUpMiscStuff.js deleted file mode 100644 index dd97285..0000000 --- a/src/views/car-pictures/posts/setUpMiscStuff.js +++ /dev/null @@ -1,16 +0,0 @@ -import { setUpScrollToTop } from "../scrollToTop.js"; - -export function setUpMiscStuff(ctx, sectionContentPromise) { - if (!ctx.user?.isModerator) { - ctx.nestedShadowRoot.querySelector('section').style['justifyContent'] = 'space-around'; - } else { - sectionContentPromise.then(() => { - const card = ctx.nestedShadowRoot.querySelector('section > .card'); - if (card) { - ctx.nestedShadowRoot.querySelector('section').style.setProperty('--card-margin-top', '14px'); - } - }); - } - - setUpScrollToTop(ctx); -} \ No newline at end of file diff --git a/src/views/car-pictures/posts/startObservingTheThirdLastCard.js b/src/views/car-pictures/posts/startObservingTheThirdLastCard.js index d7d3893..4ae73a2 100644 --- a/src/views/car-pictures/posts/startObservingTheThirdLastCard.js +++ b/src/views/car-pictures/posts/startObservingTheThirdLastCard.js @@ -18,5 +18,7 @@ export function startObservingTheThirdLastCard(ctx, localRenderNew) { // Select the elements you want to observe const thirdLastCard = ctx.nestedShadowRoot.querySelector('.card:nth-last-child(3)'); - observer.observe(thirdLastCard); + if (thirdLastCard) { + observer.observe(thirdLastCard); + } } \ No newline at end of file diff --git a/src/views/car-pictures/renderNew.js b/src/views/car-pictures/renderNew.js new file mode 100644 index 0000000..68e1cba --- /dev/null +++ b/src/views/car-pictures/renderNew.js @@ -0,0 +1,125 @@ +import { html } from "../../lib/lit-html.js"; +import { until } from "../../lib/directives/until.js"; +import { get2SearchedPostObjects, get2UnapprovedSearchedPostObjects } from "../../api/posts.js"; +import { sectionClickHandler } from "./infoWindow/infoWindow.js"; +import { carPicturesTemplate } from "./carPicturesTemplate.js"; +import { getSwitchToApprovalModeHandler } from "./posts/switchToApprovalModeHandler.js"; +import { getNoPostsTemplate } from "./posts/getNoPostsTemplate.js"; +import { getUnapprovedPostsMessageTemplate } from "./posts/unapprovedPostsMessageTemplate.js"; +import { getSectionContentTemplate } from "./posts/sectionContentTemplate.js"; +import { startObservingTheThirdLastCard } from "./posts/startObservingTheThirdLastCard.js"; +import { searchHandler } from "./search/searchHandler.js"; +import { setUpScrollToTop } from "./scrollToTop.js"; +import { generatorsObject } from "./generatorsObject.js"; +import { checkIfRenderShouldStop } from "./checkIfRenderShouldStop.js"; +import { srchForPostIdsAndInitSrchGens } from "./srchForPostsAndInitSrchGens.js"; + +const loadingTemplate = () => html` +
+

Loading posts
+

+
+` + +let miscStuffSetUp = false; + +let portionsRendered = 0; + +let unapprovedPostsMessageTemplate = null; + +export function resetState(posts) { + portionsRendered = 0; + posts.length = 0; + generatorsObject.reset(); +} + +export async function renderNew(ctx, posts, arrOfPostObjectIds = [], pageUrlSrchText, windowPath, searchHandlerText) { + if (checkIfRenderShouldStop(arrOfPostObjectIds, generatorsObject, ctx)) { + return; + } + + if ((generatorsObject.asyncSearchPostsGenerator === null && ctx.hash != 'approval-mode' + || generatorsObject.asyncUnapprovedSearchPostsGenerator === null && ctx.hash == 'approval-mode') + && arrOfPostObjectIds?.length > 0) { + + //? Get a new search posts generator with the new post object ids. + + if (ctx.hash == 'approval-mode') { + generatorsObject.asyncUnapprovedSearchPostsGenerator = get2UnapprovedSearchedPostObjects(arrOfPostObjectIds); + generatorsObject.asyncUnapprovedSearchPostsGeneratorIsDone = false; + } else { + generatorsObject.asyncSearchPostsGenerator = get2SearchedPostObjects(arrOfPostObjectIds); + generatorsObject.asyncSearchPostsGeneratorIsDone = false; + } + } + + let pageUrlSrchTextPromise = null; + if (pageUrlSrchText) { + pageUrlSrchTextPromise = srchForPostIdsAndInitSrchGens(ctx, pageUrlSrchText, arrOfPostObjectIds, generatorsObject); + } + + const sectionContentPromise = getSectionContentTemplate( + getNoPostsTemplate, + ctx.hash == 'approval-mode' ? 'unapproved' : null, + posts, + generatorsObject, + ctx, + arrOfPostObjectIds, + pageUrlSrchTextPromise, + windowPath + ); + + if (posts.length > 0) { + //? If we already have 2 posts, the displaying of the "loading" template is avoided and the posts are shown after they have loaded. + try { + await sectionContentPromise; //? we wait for the two cards to show up on the screen + } catch (error) { + //? The sectionContentPromise may reject, if views changed while some post promises were still pending. + console.error(error) + return; + } + } + + //? If we already got the message, there is no need to get it again. + if (ctx.user && (unapprovedPostsMessageTemplate === null || posts.length == 0)) { + unapprovedPostsMessageTemplate = until(getUnapprovedPostsMessageTemplate(sectionContentPromise), null); + } + + ctx.render( + carPicturesTemplate( + ev => sectionClickHandler(ev, posts, ctx), + until(sectionContentPromise, loadingTemplate()), + ctx.user?.isModerator ? getSwitchToApprovalModeHandler(ctx) : null, + ctx.user ? unapprovedPostsMessageTemplate : null, + ev => searchHandler(renderNew.bind(null, ctx, posts), ev, resetState.bind(null, posts), ctx), + ctx, + Boolean(arrOfPostObjectIds) + ) + ); + + ctx.nestedShadowRoot.querySelector('#postsSearchInput').value = pageUrlSrchText || searchHandlerText || ''; + if ((arrOfPostObjectIds == null || arrOfPostObjectIds?.length == 0) && posts.length == 0 && !pageUrlSrchText) { + } + + if (!miscStuffSetUp) { + setUpScrollToTop(ctx); + miscStuffSetUp = true; + } + + try { + await sectionContentPromise; //? we wait for the two cards to show up on the screen + } catch (error) { + //? the sectionContentPromise may reject if views change durring it "pending" state + console.error(error) + return; + } + + portionsRendered++; + + if (portionsRendered < 2) { + renderNew(ctx, posts, arrOfPostObjectIds); //? render one more portion + } else { + portionsRendered = 0; + startObservingTheThirdLastCard(ctx, renderNew.bind(null, ctx, posts, arrOfPostObjectIds)); //? When user sees the third card, repeat the rendering cycle. + } +} \ No newline at end of file diff --git a/src/views/car-pictures/search/searchHandler.js b/src/views/car-pictures/search/searchHandler.js new file mode 100644 index 0000000..023248d --- /dev/null +++ b/src/views/car-pictures/search/searchHandler.js @@ -0,0 +1,37 @@ +import { searchForPosts } from "../../../api/posts.js"; + +export async function searchHandler(renderNew, ev, resetState) { + ev.preventDefault(); + + const searchText = ev.target.querySelector('input.form-control').value.trim(); + const srchBtn = ev.target.querySelector('button[type="submit"]'); + + const wordsRegEx = /(?:(?:[a-zA-Z_]|[\u0410-\u044F])|['-](?:[a-zA-Z_]|[\u0410-\u044F])|\d+(?:\.\d+)*)+/gm; + if (!wordsRegEx.test(searchText)) { + alert("Invalid search text!"); + return; + } + + const curUrl = `?search=${searchText}${window.location.hash}`; + history.pushState({}, "", curUrl); + + //? (in getSectionContentTemplate) the window path may change while some post promises are still pending so we create this variable to prevent the rendering of posts in a wrong view + let windowPath = window.location.href.replace(window.location.origin, ''); + windowPath = window.location.hash ? windowPath : windowPath.replace("#", ''); + + srchBtn.textContent = "Searching..."; + srchBtn.disabled = true; + + const srchResultsArr = await searchForPosts(searchText); + const postObjIds = [...new Set(srchResultsArr.sort((a, b) => b.matchCoefficient - a.matchCoefficient).map(srchResObj => srchResObj.postId))]; + + srchBtn.textContent = "Search"; + srchBtn.removeAttribute('disabled'); + + if (srchResultsArr.length == 0) { + renderNew([]); //? render "nothing was found" message + } else { + resetState(); + renderNew(postObjIds, null, windowPath, searchText); + } +} \ No newline at end of file diff --git a/src/views/car-pictures/srchForPostsAndInitSrchGens.js b/src/views/car-pictures/srchForPostsAndInitSrchGens.js new file mode 100644 index 0000000..ca92f26 --- /dev/null +++ b/src/views/car-pictures/srchForPostsAndInitSrchGens.js @@ -0,0 +1,15 @@ +import { get2SearchedPostObjects, get2UnapprovedSearchedPostObjects, searchForPosts } from "../../api/posts.js"; + +export async function srchForPostIdsAndInitSrchGens(ctx, urlSrchText, arrOfPostObjectIds, generatorsObject) { + const srchResultsArr = await searchForPosts(urlSrchText); + const postObjIds = [...new Set(srchResultsArr.sort((a, b) => b.matchCoefficient - a.matchCoefficient).map(srchResObj => srchResObj.postId))]; + arrOfPostObjectIds.push(...postObjIds) + + if (ctx.hash == 'approval-mode') { + generatorsObject.asyncUnapprovedSearchPostsGenerator = get2UnapprovedSearchedPostObjects(arrOfPostObjectIds); + generatorsObject.asyncUnapprovedSearchPostsGeneratorIsDone = false; + } else { + generatorsObject.asyncSearchPostsGenerator = get2SearchedPostObjects(arrOfPostObjectIds); + generatorsObject.asyncSearchPostsGeneratorIsDone = false; + } +} \ No newline at end of file