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 navOpened = false;
let oldNavText = ""; let oldNavText = "";
let oldNavIcon = ""; let oldNavIcon = "";
@ -9,6 +12,7 @@ let navContent = document.getElementById('nav-content');
let mosaic = document.getElementById('mosaic'); let mosaic = document.getElementById('mosaic');
let mosaicHeader = document.getElementById('mosaic-header'); let mosaicHeader = document.getElementById('mosaic-header');
let tags = []
navEnabler.onclick = async () => { navEnabler.onclick = async () => {
if (!navOpened) { if (!navOpened) {
@ -34,6 +38,9 @@ function createEl(className = false, elName = "div") {
return el; return el;
} }
/**
* Render
*/
function renderNavItem(tag) { function renderNavItem(tag) {
/* /*
<div class="nav-item"> <div class="nav-item">
@ -87,6 +94,7 @@ function renderCard(organization) {
// image // image
let image = createEl('card-image-container') let image = createEl('card-image-container')
let imageTag = createEl('card-image') let imageTag = createEl('card-image')
//mediaBaseUrl + '/' +
imageTag.style = `background-image: url('${organization.thumbnail}')` imageTag.style = `background-image: url('${organization.thumbnail}')`
image.appendChild(imageTag) image.appendChild(imageTag)
card.appendChild(image) card.appendChild(image)
@ -102,14 +110,15 @@ function renderCard(organization) {
let icon = createEl('card-icon') let icon = createEl('card-icon')
if (Array.isArray(organization.tags) && organization.tags.length > 0) { if (Array.isArray(organization.tags) && organization.tags.length > 0) {
let tag = tags.filter(tag => organization.tags[0] === tag._id)[0] let tag = tags.filter(tag => organization.tags[0] === tag._id)[0]
icon.innerHTML = `<svg // icon.innerHTML = `<svg
aria-hidden="true" // aria-hidden="true"
focusable="false" // focusable="false"
role="img" // role="img"
xmlns="http://www.w3.org/2000/svg" // xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 ${tag.icon.width} ${tag.icon.height}"> // viewBox="0 0 ${tag.icon.width} ${tag.icon.height}">
<path fill="currentColor" d="${tag.icon.path}"></path> // <path fill="currentColor" d="${tag.icon.path}"></path>
</svg>` // </svg>`
icon.innerHTML = tag.iconHTML
} }
titleContainer.appendChild(icon) titleContainer.appendChild(icon)
upperContent.appendChild(titleContainer) upperContent.appendChild(titleContainer)
@ -117,8 +126,8 @@ function renderCard(organization) {
let description = createEl('card-description') let description = createEl('card-description')
description.textContent = organization.description description.textContent = organization.description
let goTo = "/association/" + organization.slugs[organization.slugs.length - 1] let goTo = "/association/" + organization.slug
if (organization.isProposed) { if (isProposed) {
goTo += "?version=proposed" goTo += "?version=proposed"
} }
// let link = createEl('card-link') // let link = createEl('card-link')
@ -141,13 +150,6 @@ function renderCard(organization) {
return card return card
} }
function renderMosaic(data) {
let cardContainer = createEl('card-container')
data.forEach(orga => {
cardContainer.appendChild(renderCard(orga))
})
return cardContainer
}
let currentTag = null let currentTag = null
let currentCardContainer = null let currentCardContainer = null
@ -159,12 +161,7 @@ function enableTag(node) {
tagId = node.attributes['data-tag-id'].value tagId = node.attributes['data-tag-id'].value
} }
let data = organizations.filter(orga => orga.tags.filter(id => id === tagId).length > 0 || all) let data = organizations.filter(orga => orga.tags.filter(id => id === tagId).length > 0 || all)
let cards = renderMosaic(data) renderMosaic(data)
if (currentCardContainer !== null) {
mosaic.removeChild(currentCardContainer)
}
currentCardContainer = cards
mosaic.appendChild(cards)
node.className += ' enabled' node.className += ' enabled'
if (currentTag !== null) { if (currentTag !== null) {
currentTag.className = currentTag.className.replace('enabled', '') 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;
enableTag(document.getElementById('nav-all')) 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 { 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) { static delete(key: string, context: string) {
@ -36,7 +36,7 @@ export default class MediaService {
static deleteMany(keys: string[], context: string) { static deleteMany(keys: string[], context: string) {
console.log('> MediaCleanup: in context "' + context + '" deleteMany', keys) console.log('> MediaCleanup: in context "' + context + '" deleteMany', keys)
keys.forEach((key: string) => { keys.forEach((key: string) => {
if (key === undefined || key === null || key.length <= 2) { return } if (key == null || key.length <= 2) { return }
MediaService.delete(key, context) MediaService.delete(key, context)
}) })
} }
@ -75,4 +75,8 @@ export default class MediaService {
type: type === 'media' ? file.contentType.split('/')[0] : type 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') console.log('> App: Connected to mongodb')
}) })
app.set("twig options", { app.set("twig options", {
allow_async: true, allow_async: true,
strict_variables: false strict_variables: false

View file

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

View file

@ -11,6 +11,7 @@ import Tag from '../models/Tag'
import ErrorController from './ErrorController' import ErrorController from './ErrorController'
import Utils from '../Utils' import Utils from '../Utils'
import mongoose from 'mongoose' import mongoose from 'mongoose'
import MediaService from '../MediaService'
export default class PublicController { export default class PublicController {
@ -30,24 +31,29 @@ export default class PublicController {
if (!isProposed) { if (!isProposed) {
organizations = organizations.filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null) 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', { res.render('home.twig', {
isProposed, isProposed,
mediaBaseUrl: MediaService.getMediaBaseUrl(),
tags, tags,
tagsJSON: JSON.stringify(tags), tagsJSON: JSON.stringify(tags),
organizationsJSON: JSON.stringify(organizations organizationsJSON: JSON.stringify(organizationsData)
.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
}
})
)
}) })
}).catch(err => () => { }).catch(err => () => {
console.log(err) console.log(err)

View file

@ -33,7 +33,12 @@
Toutes Toutes
</div> </div>
<div class="nav-access"> <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> </div>
</div> </div>
@ -72,7 +77,8 @@
{% block scripts %} {% block scripts %}
<script> <script>
let tags = JSON.parse(`{{ tagsJSON }}`) let mediaBaseUrl = "{{ mediaBaseUrl }}"
let isProposed = {{ isProposed }}
let organizations = JSON.parse(`{{ organizationsJSON }}`) let organizations = JSON.parse(`{{ organizationsJSON }}`)
</script> </script>
<script src="/scripts/home.js"></script> <script src="/scripts/home.js"></script>