feat: add lazy loading of card on home page

feat: add universal organization store
fix: optimization for JSON data loaded in home page HTML
This commit is contained in:
lefuturiste 2020-07-26 13:35:03 +00:00
parent 2e6e64a6d3
commit ecf9a0720e
8 changed files with 224 additions and 87 deletions

View file

@ -1,3 +1,6 @@
/**
* Nav management
*/
let navOpened = false;
let oldNavText = "";
let oldNavIcon = "";
@ -9,6 +12,7 @@ let navContent = document.getElementById('nav-content');
let mosaic = document.getElementById('mosaic');
let mosaicHeader = document.getElementById('mosaic-header');
let tags = []
navEnabler.onclick = async () => {
if (!navOpened) {
@ -34,6 +38,9 @@ function createEl(className = false, elName = "div") {
return el;
}
/**
* Render
*/
function renderNavItem(tag) {
/*
<div class="nav-item">
@ -87,6 +94,7 @@ function renderCard(organization) {
// image
let image = createEl('card-image-container')
let imageTag = createEl('card-image')
//mediaBaseUrl + '/' +
imageTag.style = `background-image: url('${organization.thumbnail}')`
image.appendChild(imageTag)
card.appendChild(image)
@ -102,14 +110,15 @@ function renderCard(organization) {
let icon = createEl('card-icon')
if (Array.isArray(organization.tags) && organization.tags.length > 0) {
let tag = tags.filter(tag => organization.tags[0] === tag._id)[0]
icon.innerHTML = `<svg
aria-hidden="true"
focusable="false"
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 ${tag.icon.width} ${tag.icon.height}">
<path fill="currentColor" d="${tag.icon.path}"></path>
</svg>`
// icon.innerHTML = `<svg
// aria-hidden="true"
// focusable="false"
// role="img"
// xmlns="http://www.w3.org/2000/svg"
// viewBox="0 0 ${tag.icon.width} ${tag.icon.height}">
// <path fill="currentColor" d="${tag.icon.path}"></path>
// </svg>`
icon.innerHTML = tag.iconHTML
}
titleContainer.appendChild(icon)
upperContent.appendChild(titleContainer)
@ -117,8 +126,8 @@ function renderCard(organization) {
let description = createEl('card-description')
description.textContent = organization.description
let goTo = "/association/" + organization.slugs[organization.slugs.length - 1]
if (organization.isProposed) {
let goTo = "/association/" + organization.slug
if (isProposed) {
goTo += "?version=proposed"
}
// let link = createEl('card-link')
@ -141,13 +150,6 @@ function renderCard(organization) {
return card
}
function renderMosaic(data) {
let cardContainer = createEl('card-container')
data.forEach(orga => {
cardContainer.appendChild(renderCard(orga))
})
return cardContainer
}
let currentTag = null
let currentCardContainer = null
@ -159,12 +161,7 @@ function enableTag(node) {
tagId = node.attributes['data-tag-id'].value
}
let data = organizations.filter(orga => orga.tags.filter(id => id === tagId).length > 0 || all)
let cards = renderMosaic(data)
if (currentCardContainer !== null) {
mosaic.removeChild(currentCardContainer)
}
currentCardContainer = cards
mosaic.appendChild(cards)
renderMosaic(data)
node.className += ' enabled'
if (currentTag !== null) {
currentTag.className = currentTag.className.replace('enabled', '')
@ -179,8 +176,113 @@ function enableTag(node) {
}
}
navContent.childNodes.forEach(node => {
node.onclick = () => enableTag(node)
})
/***
* Is a element in the view ?
*/
function posY(elm) {
var test = elm, top = 0;
while(!!test && test.tagName.toLowerCase() !== "body") {
top += test.offsetTop;
test = test.offsetParent;
}
return top;
}
function viewPortHeight() {
var de = document.documentElement;
if(!!window.innerWidth)
{ return window.innerHeight; }
else if( de && !isNaN(de.clientHeight) )
{ return de.clientHeight; }
return 0;
}
function scrollY() {
if( window.pageYOffset ) { return window.pageYOffset; }
return Math.max(document.documentElement.scrollTop, document.body.scrollTop);
}
function isVisible(elm) {
var vpH = viewPortHeight(), // Viewport Height
st = scrollY(), // Scroll Top
y = posY(elm);
return !(y > (vpH + st));
}
/**
* RenderMosaic
* (take all the organizations we want to render)
* Render only the first 5 elements
* Set the focus point on the last - 1 of theses elements
* When the focus point is on screen we load the next 5 elements
*/
let rendering = true
let page = 0
let elementsPerPage = 5
let focusPoint = null
let focusElementPos = 2
let cardContainer = null
let currentOrganizations = []
let pageCount = 0
function renderPage() {
rendering = true
let data = currentOrganizations.slice(page * elementsPerPage, (page + 1) * elementsPerPage)
data.forEach((orga, index) => {
let card = renderCard(orga)
cardContainer.appendChild(card)
if (index === data.length - focusElementPos) {
focusPoint = card
}
})
rendering = false
}
function renderMosaic(data) {
cardContainer = createEl('card-container')
// 1 - parse all the data
// 2 - for each
currentOrganizations = data
pageCount = Math.floor(data.length / elementsPerPage)
renderPage()
if (currentCardContainer !== null) {
mosaic.removeChild(currentCardContainer)
}
currentCardContainer = cardContainer
mosaic.appendChild(cardContainer)
}
window.onscroll = () => {
if (focusPoint != null) {
//console.log(isVisible(focusPoint))
if (isVisible(focusPoint) && !rendering) {
if ((page + 1) < pageCount) {
page++
renderPage()
}
}
}
}
/**
* Fetch tags and register click handler
*/
window.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('#nav-content .nav-item').forEach(node => {
node.onclick = () => enableTag(node)
if (node.id === 'nav-all') {
return
}
tags.push({
_id: node.attributes['data-tag-id'].value,
iconHTML: node.querySelector('.nav-icon').innerHTML
})
})
enableTag(document.getElementById('nav-all'))
})

View file

@ -1 +1 @@
let navOpened=!1,oldNavText="",oldNavIcon="",navEnabler=document.getElementById("nav-enabler"),navEnablerText=document.getElementById("nav-enabler-text"),navEnablerIcon=document.getElementById("nav-enabler-icon"),navContent=document.getElementById("nav-content"),mosaic=document.getElementById("mosaic"),mosaicHeader=document.getElementById("mosaic-header");function createEl(e=!1,t="div"){let n=document.createElement(t);return 0!=e&&(n.className=e),n}function renderNavItem(e){let t=createEl("nav-item"),n=createEl("nav-icon"),a=createEl(e.icon,"i");n.appendChild(a),t.appendChild(n);let r=createEl("nav-item-content"),l=createEl("nav-title");l.textContent=e.name,r.appendChild(l);let i=createEl("nav-access"),c=createEl("fas fa-chevron-right","i");return i.appendChild(c),r.appendChild(l),r.appendChild(i),t.appendChild(r),t}function setAttributes(e,t){for(var n in t)attr=document.createAttribute(n),attr.value=t[n],e.attributes.setNamedItem(attr)}function renderCard(e){let t=createEl("card","a"),n=createEl("card-image-container"),a=createEl("card-image");a.style=`background-image: url('${e.thumbnail}')`,n.appendChild(a),t.appendChild(n);let r=createEl("card-content"),l=createEl(),i=createEl("card-title-container"),c=createEl("card-title","h2");c.textContent=e.name,i.appendChild(c);let d=createEl("card-icon");if(Array.isArray(e.tags)&&e.tags.length>0){let t=tags.filter(t=>e.tags[0]===t._id)[0];d.innerHTML=`<svg\n aria-hidden="true"\n focusable="false"\n role="img"\n xmlns="http://www.w3.org/2000/svg"\n viewBox="0 0 ${t.icon.width} ${t.icon.height}">\n <path fill="currentColor" d="${t.icon.path}"></path>\n </svg>`}i.appendChild(d),l.appendChild(i);let o=createEl("card-description");o.textContent=e.description;let s="/association/"+e.slugs[e.slugs.length-1];return e.isProposed&&(s+="?version=proposed"),l.appendChild(o),r.appendChild(l),t.appendChild(r),t.href=s,t}function renderMosaic(e){let t=createEl("card-container");return e.forEach(e=>{t.appendChild(renderCard(e))}),t}navEnabler.onclick=async()=>{navOpened?(navEnablerText.textContent=oldNavText,navEnablerIcon.style.transform="rotate(0deg)",navContent.style.maxHeight=null):(oldNavText=navEnablerText.textContent,navEnablerText.textContent="Minimiser le menu",navEnablerIcon.style.transform="rotate(90eg)",navContent.style.maxHeight=navContent.scrollHeight+"px"),navOpened=!navOpened};let currentTag=null,currentCardContainer=null;function enableTag(e){let t="nav-all"===e.id,n="";t||(n=e.attributes["data-tag-id"].value);let a=organizations.filter(e=>e.tags.filter(e=>e===n).length>0||t),r=renderMosaic(a);null!==currentCardContainer&&mosaic.removeChild(currentCardContainer),currentCardContainer=r,mosaic.appendChild(r),e.className+=" enabled",null!==currentTag&&(currentTag.className=currentTag.className.replace("enabled","")),currentTag=e,null==a||a.length<=0?mosaicHeader.textContent="Aucune associations listées":1===a.length?mosaicHeader.textContent="Une association listée":mosaicHeader.textContent=a.length+" associations listées"}navContent.childNodes.forEach(e=>{e.onclick=()=>enableTag(e)}),enableTag(document.getElementById("nav-all"));
let navOpened=!1,oldNavText="",oldNavIcon="",navEnabler=document.getElementById("nav-enabler"),navEnablerText=document.getElementById("nav-enabler-text"),navEnablerIcon=document.getElementById("nav-enabler-icon"),navContent=document.getElementById("nav-content"),mosaic=document.getElementById("mosaic"),mosaicHeader=document.getElementById("mosaic-header"),tags=[];function createEl(e=!1,n="div"){let t=document.createElement(n);return 0!=e&&(t.className=e),t}function renderNavItem(e){let n=createEl("nav-item"),t=createEl("nav-icon"),a=createEl(e.icon,"i");t.appendChild(a),n.appendChild(t);let r=createEl("nav-item-content"),l=createEl("nav-title");l.textContent=e.name,r.appendChild(l);let o=createEl("nav-access"),i=createEl("fas fa-chevron-right","i");return o.appendChild(i),r.appendChild(l),r.appendChild(o),n.appendChild(r),n}function setAttributes(e,n){for(var t in n)attr=document.createAttribute(t),attr.value=n[t],e.attributes.setNamedItem(attr)}function renderCard(e){let n=createEl("card","a"),t=createEl("card-image-container"),a=createEl("card-image");a.style=`background-image: url('${e.thumbnail}')`,t.appendChild(a),n.appendChild(t);let r=createEl("card-content"),l=createEl(),o=createEl("card-title-container"),i=createEl("card-title","h2");i.textContent=e.name,o.appendChild(i);let c=createEl("card-icon");if(Array.isArray(e.tags)&&e.tags.length>0){let n=tags.filter(n=>e.tags[0]===n._id)[0];c.innerHTML=n.iconHTML}o.appendChild(c),l.appendChild(o);let d=createEl("card-description");d.textContent=e.description;let s="/association/"+e.slug;return isProposed&&(s+="?version=proposed"),l.appendChild(d),r.appendChild(l),n.appendChild(r),n.href=s,n}navEnabler.onclick=async()=>{navOpened?(navEnablerText.textContent=oldNavText,navEnablerIcon.style.transform="rotate(0deg)",navContent.style.maxHeight=null):(oldNavText=navEnablerText.textContent,navEnablerText.textContent="Minimiser le menu",navEnablerIcon.style.transform="rotate(90eg)",navContent.style.maxHeight=navContent.scrollHeight+"px"),navOpened=!navOpened};let currentTag=null,currentCardContainer=null;function enableTag(e){let n="nav-all"===e.id,t="";n||(t=e.attributes["data-tag-id"].value);let a=organizations.filter(e=>e.tags.filter(e=>e===t).length>0||n);renderMosaic(a),e.className+=" enabled",null!==currentTag&&(currentTag.className=currentTag.className.replace("enabled","")),currentTag=e,null==a||a.length<=0?mosaicHeader.textContent="Aucune associations listées":1===a.length?mosaicHeader.textContent="Une association listée":mosaicHeader.textContent=a.length+" associations listées"}function posY(e){for(var n=e,t=0;n&&"body"!==n.tagName.toLowerCase();)t+=n.offsetTop,n=n.offsetParent;return t}function viewPortHeight(){var e=document.documentElement;return window.innerWidth?window.innerHeight:e&&!isNaN(e.clientHeight)?e.clientHeight:0}function scrollY(){return window.pageYOffset?window.pageYOffset:Math.max(document.documentElement.scrollTop,document.body.scrollTop)}function isVisible(e){var n=viewPortHeight(),t=scrollY();return!(posY(e)>n+t)}let rendering=!0,page=0,elementsPerPage=5,focusPoint=null,focusElementPos=2,cardContainer=null,currentOrganizations=[],pageCount=0;function renderPage(){rendering=!0;let e=currentOrganizations.slice(page*elementsPerPage,(page+1)*elementsPerPage);e.forEach((n,t)=>{let a=renderCard(n);cardContainer.appendChild(a),t===e.length-focusElementPos&&(focusPoint=a)}),rendering=!1}function renderMosaic(e){cardContainer=createEl("card-container"),currentOrganizations=e,pageCount=Math.floor(e.length/elementsPerPage),renderPage(),null!==currentCardContainer&&mosaic.removeChild(currentCardContainer),currentCardContainer=cardContainer,mosaic.appendChild(cardContainer)}window.onscroll=()=>{null!=focusPoint&&isVisible(focusPoint)&&!rendering&&page+1<pageCount&&(page++,renderPage())},window.addEventListener("DOMContentLoaded",()=>{document.querySelectorAll("#nav-content .nav-item").forEach(e=>{e.onclick=()=>enableTag(e),"nav-all"!==e.id&&tags.push({_id:e.attributes["data-tag-id"].value,iconHTML:e.querySelector(".nav-icon").innerHTML})}),enableTag(document.getElementById("nav-all"))});

File diff suppressed because one or more lines are too long

View file

@ -18,7 +18,7 @@ export default class MediaService {
}
static getBucket(): string {
return process.env.S3_BUCKET === undefined ? '___BUCKET_NOT_FOUND_ENV_VAR_ISSUE___' : process.env.S3_BUCKET
return process.env.S3_BUCKET == null ? '___BUCKET_NOT_FOUND_ENV_VAR_ISSUE___' : process.env.S3_BUCKET
}
static delete(key: string, context: string) {
@ -36,7 +36,7 @@ export default class MediaService {
static deleteMany(keys: string[], context: string) {
console.log('> MediaCleanup: in context "' + context + '" deleteMany', keys)
keys.forEach((key: string) => {
if (key === undefined || key === null || key.length <= 2) { return }
if (key == null || key.length <= 2) { return }
MediaService.delete(key, context)
})
}
@ -75,4 +75,8 @@ export default class MediaService {
type: type === 'media' ? file.contentType.split('/')[0] : type
}
}
static getMediaBaseUrl() {
return process.env.S3_BASE_URL == null ? '___BUCKET_BASE_URL_NOT_FOUND_ENV_VAR_ISSUE___' : process.env.S3_BASE_URL
}
}

View file

@ -50,7 +50,6 @@ let main = async () => {
console.log('> App: Connected to mongodb')
})
app.set("twig options", {
allow_async: true,
strict_variables: false

View file

@ -29,15 +29,33 @@ export default class AdminOrganizationController {
}
static store(req: express.Request, res: express.Response) {
AdminOrganizationController.storeUniversal(
req.body.adminName,
req.body.email,
req.body.validationState
).then(({ data, body }) => {
res.json({ success: true, data, body })
}).catch((err: any) => {
res.status(400).json({ success: false, errors: err.errors })
})
}
static storeUniversal(adminName: string, email: string, validationState: string): Promise<any> {
return new Promise((resolve, reject) => {
if (validationState == null) {
validationState = 'unaware'
}
let body: any = {
token: AdminOrganizationController.generateToken(),
createdAt: new Date(),
// start the slugs array
slugs: [slugify(req.body.adminName)],
...req.body,
slugs: [slugify(adminName)],
validationState,
adminName,
email,
...{
proposedVersion: {
name: req.body.adminName,
name: adminName,
contacts: {
facebook: '',
twitter: '',
@ -45,7 +63,7 @@ export default class AdminOrganizationController {
website: '',
address: '',
person: '',
email: req.body.email,
email,
phone: ''
}
// descriptionShort: '',
@ -60,10 +78,12 @@ export default class AdminOrganizationController {
}
}
}
console.log(body)
Organization.create(body).then(data => {
AdminOrganizationController.sendEmailTokenUniversal(data)
res.json({ success: true, data, body })
}).catch(err => res.status(400).json({ success: false, errors: err.errors }))
resolve({ data, body })
}).catch(err => reject(err))
})
}
static update(req: express.Request, res: express.Response) {
@ -172,12 +192,12 @@ export default class AdminOrganizationController {
let extra: any = {}
let slug = slugify(proposedVersion.name)
// only add this slug if the proposed slug is not found in the list of current slug
let currentSlugs: string[] = []
extra.slugs = []
if (Array.isArray(data.get('slugs'))) {
currentSlugs = data.get('slugs')
extra.slugs = data.get('slugs')
}
if (currentSlugs.filter(s => s === slug).length === 0) {
extra.slugs = currentSlugs.concat([slug])
if (extra.slugs.filter((s: any) => s === slug).length === 0) {
extra.slugs = extra.slugs.concat([slug])
}
extra.adminName = proposedVersion.name
@ -231,12 +251,12 @@ export default class AdminOrganizationController {
updatedAt: new Date()
}).then(updateData => {
EmailService.send(
data.get('email'),
extra.email,
"Félicitations, vos changements ont été approuvés et publiés !",
"published",
{
adminName: data.get('adminName'),
link: EmailService.getBaseUrl() + '/association/' + data.get('slug')
link: EmailService.getBaseUrl() + '/association/' + extra.slugs[extra.slugs.length - 1]
}
)
res.json({ success: true, data: updateData })

View file

@ -11,6 +11,7 @@ import Tag from '../models/Tag'
import ErrorController from './ErrorController'
import Utils from '../Utils'
import mongoose from 'mongoose'
import MediaService from '../MediaService'
export default class PublicController {
@ -30,24 +31,29 @@ export default class PublicController {
if (!isProposed) {
organizations = organizations.filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null)
}
// let organizationsData = organizations
// .map(o => {
// const version = isProposed ? o.get('proposedVersion'): o.get('publishedVersion')
// return {
// _id: o._id,
// name: version.name,
// description: version.descriptionShort,
// thumbnail: version.thumbnail.key,
// tags: version.tags === null ? 'tags_not_found' : version.tags,
// slug: o.get('slugs')[o.get('slugs).length -1]
// }
// })
let lorem = "Dolore sit tempor et duo ipsum sit sed takimata, et magna voluptua ut sed justo eirmod. Sed tempor justo magna accusam aliquyam sea invidunt eos. Aliquyam accusam vero accusam sed"
let organizationsData = []
for (var i = 0; i < 40; i++) {
organizationsData.push({ _id: i, name: 'Item ' + i, description: lorem, thumbnail: 'https://picsum.photos/800?hash=' + i, slug: 'qsd-'+i, tags: ['5f0e00e1a4dbfe3e0b5291d2'] })
}
res.render('home.twig', {
isProposed,
mediaBaseUrl: MediaService.getMediaBaseUrl(),
tags,
tagsJSON: JSON.stringify(tags),
organizationsJSON: JSON.stringify(organizations
.map(o => {
const version = isProposed ? o.get('proposedVersion'): o.get('publishedVersion')
return {
_id: o._id,
name: version.name,
description: version.descriptionShort,
thumbnail: version.thumbnail.location,
tags: version.tags === null ? 'tags_not_found' : version.tags,
slugs: o.get('slugs'),
isProposed
}
})
)
organizationsJSON: JSON.stringify(organizationsData)
})
}).catch(err => () => {
console.log(err)

View file

@ -33,7 +33,12 @@
Toutes
</div>
<div class="nav-access">
<i class="fas fa-chevron-right"></i>
<svg aria-hidden="true" focusable="false" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512">
<path
fill="currentColor"
d="M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z"
></path>
</svg>
</div>
</div>
</div>
@ -72,7 +77,8 @@
{% block scripts %}
<script>
let tags = JSON.parse(`{{ tagsJSON }}`)
let mediaBaseUrl = "{{ mediaBaseUrl }}"
let isProposed = {{ isProposed }}
let organizations = JSON.parse(`{{ organizationsJSON }}`)
</script>
<script src="/scripts/home.js"></script>