From 0c881c919bc7edfc66f118c8392eeba0362af4b0 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 19 Jul 2020 13:26:57 +0000 Subject: [PATCH] update --- package.json | 2 + src/Utils.ts | 6 +- src/app.ts | 14 +++-- .../AdminOrganizationController.ts | 52 ++++++++++++----- src/controllers/DelegateController.ts | 56 +++++++++++++------ src/controllers/ErrorController.ts | 6 ++ src/controllers/PublicController.ts | 54 ++++++++++++++---- src/models/Organization.ts | 4 +- static/assets/home.css | 12 ++-- static/assets/js/home.js | 43 +++++++------- static/assets/main.css | 29 ++++++++++ static/assets/organization.css | 18 +++++- views/base.twig | 12 ++++ views/organization.twig | 23 ++++++-- yarn.lock | 12 ++++ 15 files changed, 260 insertions(+), 83 deletions(-) diff --git a/package.json b/package.json index 245f19e..3735fd6 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@types/html-to-text": "^5.1.1", "@types/ioredis": "^4.17.0", "@types/jest": "^26.0.4", + "@types/moment": "^2.13.0", "@types/mongoose": "^5.7.28", "@types/multer": "^1.4.3", "@types/multer-s3": "^2.7.7", @@ -26,6 +27,7 @@ "html-to-text": "^5.1.1", "ioredis": "^4.17.3", "jest": "^26.1.0", + "moment": "^2.27.0", "mongoose": "^5.9.20", "multer": "^1.4.2", "multer-s3": "^2.9.0", diff --git a/src/Utils.ts b/src/Utils.ts index bac9581..3e2afd2 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -17,9 +17,9 @@ export default class Utils { bool = bool && ( explored !== undefined && explored !== null ) - if (i - 1 === levels && isStr) { - bool = bool && typeof explored === 'string' && explored.length > 0 - } + } + if (isStr) { + bool = bool && typeof explored === 'string' && explored.length > 0 } return bool } diff --git a/src/app.ts b/src/app.ts index 26f5573..e8a3fed 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,12 @@ import twig from 'twig' import EmailService from './EmailService' import ErrorController from './controllers/ErrorController' +process.on('unhandledRejection', (err) => { + console.error(err) + console.log('unhandledRejection!') + process.exit() +}) + dotenv.config({ path: __dirname + '/../.env' }) @@ -110,11 +116,7 @@ let main = async () => { app.get('/500', ErrorController.internalError) - app.use((err: any, req: express.Request, res: express.Response, next: express.RequestHandler) => { - console.error(err.stack) - //res.status(res.statusCode).json({ success: false, error: err.stack }) - ErrorController.handle(500, 'Erreur interne', 'Ouups je sais pas quoi dire', '💥', err.stack)(req, res) - }) + app.use(ErrorController.handleServerError) app.use(ErrorController.notFoundHandler()) @@ -123,4 +125,4 @@ let main = async () => { }) } -main() \ No newline at end of file +main() diff --git a/src/controllers/AdminOrganizationController.ts b/src/controllers/AdminOrganizationController.ts index 4c085a1..7461d77 100644 --- a/src/controllers/AdminOrganizationController.ts +++ b/src/controllers/AdminOrganizationController.ts @@ -30,11 +30,22 @@ export default class AdminOrganizationController { let body: any = { token: AdminOrganizationController.generateToken(), createdAt: new Date(), - slug: slugify(req.body.adminName).toLowerCase(), + // start the slugs array + slugs: [slugify(req.body.adminName)], ...req.body, ...{ proposedVersion: { - name: req.body.adminName + name: req.body.adminName, + contacts: { + facebook: '', + twitter: '', + instagram: '', + website: '', + address: '', + person: '', + email: req.body.email, + phone: '' + } // descriptionShort: '', // descriptionLong: '', // contacts: [], @@ -49,22 +60,35 @@ export default class AdminOrganizationController { } Organization.create(body).then(data => { AdminOrganizationController.sendEmailTokenUniversal(data) - res.json({ success: true, data }) + res.json({ success: true, data, body }) }).catch(err => res.status(400).json({ success: false, errors: err.errors })) } static update(req: express.Request, res: express.Response) { - let extra: any = {} - if (req.body.name !== undefined) { - extra.slug = slugify(req.body.name) - } - Organization.updateOne({ _id: req.params.id }, { - ...extra, - ...req.body, - updatedAt: new Date() - }) - .then(data => res.json({ success: true, data })) - .catch(err => res.status(400).json({ success: false, errors: err.errors !== undefined ? err.errors : err })) + Organization.findById(req.params.id) + .then(data => { + // generate extra slugs + let extra: any = {} + if (req.body.name !== undefined) { + let slug = slugify(req.body.name) + // only add this slug if the proposed slug is not found in the list of current slug + let currentSlugs: string[] = [] + if (data !== null && Array.isArray(data.get('slugs'))) { + currentSlugs = data.get('slugs') + } + if (currentSlugs.filter(s => s === slug).length === 0) { + extra.slugs = currentSlugs.concat([slug]) + } + } + Organization.updateOne({ _id: req.params.id }, { + ...extra, + ...req.body, + updatedAt: new Date() + }) + .then(data => res.json({ success: true, data })) + .catch(err => res.status(400).json({ success: false, errors: err.errors !== undefined ? err.errors : err })) + }) + .catch(err => res.status(400).json({ success: false, errors: err })) } static destroy(req: express.Request, res: express.Response) { diff --git a/src/controllers/DelegateController.ts b/src/controllers/DelegateController.ts index 0cd1a09..ec8070b 100644 --- a/src/controllers/DelegateController.ts +++ b/src/controllers/DelegateController.ts @@ -34,15 +34,19 @@ export default class DelegateController { }) } - const next = (tag: any) => { + const next = (tags: any) => { // only update proposedVersion let proposedVersion: any = req.body - proposedVersion.tag = tag - proposedVersion.descriptionLong = proposedVersion.descriptionLong.replace(/\n/g, '') - proposedVersion.descriptionLong = sanitizeHtml( - proposedVersion.descriptionLong, - { allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'li', 'b', 'i', 'abbr', 'strong', 'em', 'hr', 'br', 'div', 'pre'] } - ) + proposedVersion.tags = tags + + // sanitize long description + if (Utils.isStrUsable(proposedVersion.descriptionLong)) { + proposedVersion.descriptionLong = proposedVersion.descriptionLong.replace(/\n/g, '') + proposedVersion.descriptionLong = sanitizeHtml( + proposedVersion.descriptionLong, + { allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', 'li', 'b', 'i', 'abbr', 'strong', 'em', 'hr', 'br', 'div', 'pre'] } + ) + } // validate contact.address // validate all fields to not overflow @@ -73,6 +77,19 @@ export default class DelegateController { MediaService.delete(organization.proposedVersion.cover.key, 'coverDeleted') } + // delete unused thumbnail + // if the thumbnail is reset we must delete the old one only if it is unused in the published version + if ( + Utils.isStrUsable(organization, 'proposedVersion.thumbnail.key') && + !Utils.isStrUsable(proposedVersion, 'thumbnail.key') && + !( + Utils.isStrUsable(organization, 'publishedVersion.thumbnail.key') && + organization.publishedVersion.thumbnail.key === organization.proposedVersion.thumbnail.key + ) + ) { + MediaService.delete(organization.proposedVersion.thumbnail.key, 'thumbnailDeleted') + } + // format schedule, pricing if (!Array.isArray(proposedVersion.schedule)) { proposedVersion.schedule = [] @@ -93,22 +110,27 @@ export default class DelegateController { }).catch(err => res.status(400).json({ success: false, errors: err.errors !== undefined ? err.errors : err })) } - if (req.body.tag !== undefined && req.body.tag !== null && req.body.tag.length > 2) { + if (Utils.isUsable(req.body, 'tags') && Array.isArray(req.body.tags)) { // skip the tag part if the tag didn't changed if ( - organization.proposedVersion.tag === undefined || - organization.proposedVersion.tag === null || - req.body.tag !== organization.proposedVersion.tag._id + organization.proposedVersion.tags === undefined || + organization.proposedVersion.tags === null || + req.body.tags !== organization.proposedVersion.tags ) { // if the tag is defined, search the tag id - Tag.findById(req.body.tag).then(tag => { - next(tag) - }).catch(err => { - console.log(err) - res.status(400).json({ success: false, errors: err, _note: 'The tag id provided is invalid' }) + let promises: Promise[] = [] + req.body.tags.forEach((id: string) => { + promises.push(new Promise((resolve, reject) => { + Tag.findById(id).then((tag: any) => resolve(tag.get('_id'))).catch((err: any) => reject(err)) + })) + }) + Promise.all(promises).then(() => { + next(req.body.tags) + }).catch((err) => { + return res.status(400).json({ success: false, errors: err, _note: 'One of the tag id provided is invalid' }) }) } else { - next(organization.tag) + next(organization.tags) } } else { next(null) diff --git a/src/controllers/ErrorController.ts b/src/controllers/ErrorController.ts index 1ecb0ae..b2c6ce2 100644 --- a/src/controllers/ErrorController.ts +++ b/src/controllers/ErrorController.ts @@ -13,6 +13,12 @@ export default class ErrorController { } } + static handleServerError(err: any, req: express.Request, res: express.Response, next: any) { + console.error(err.stack) + //res.status(res.statusCode).json({ success: false, error: err.stack }) + return ErrorController.handle(500, 'Erreur interne', 'Ouups je sais pas quoi dire', '💥', err.stack)(req, res) + } + static notFoundHandler() { return ErrorController.handle(404, 'Page introuvable', 'Mais où peut donc se trouver cette page ?', '🔍 🕵️') } diff --git a/src/controllers/PublicController.ts b/src/controllers/PublicController.ts index 96c69bb..a4c03a5 100644 --- a/src/controllers/PublicController.ts +++ b/src/controllers/PublicController.ts @@ -9,6 +9,8 @@ import { IconService, IconInterface } from '../IconService' import IORedis from 'ioredis' import Tag from '../models/Tag' import ErrorController from './ErrorController' +import Utils from '../Utils' +import mongoose from 'mongoose' export default class PublicController { @@ -19,45 +21,67 @@ export default class PublicController { // data: await client.get('hello') // }) Tag.find().then(tags => { - Organization.find().then(organizations => { + let isProposed = Utils.isStrUsable(req.query, 'only') + if (isProposed && !mongoose.Types.ObjectId.isValid(req.query.only)) { + return ErrorController.handleServerError({ stack: 'Invalid object id in only query param'}, req, res, []) + } + Organization.find(isProposed ? { _id: req.query.only } : {}).then(organizations => { + if (!isProposed) { + organizations = organizations.filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null) + } res.render('home.twig', { + isProposed, tags, tagsJSON: JSON.stringify(tags), organizationsJSON: JSON.stringify(organizations - .filter(o => o.get('publishedAt') !== undefined && o.get('publishedAt') !== null) .map(o => { - const version = o.get('publishedVersion') + const version = isProposed ? o.get('proposedVersion'): o.get('publishedVersion') return { + _id: o._id, name: version.name, description: version.descriptionShort, thumbnail: version.thumbnail.location, - tag: version.tag === null ? 'tag_not_found' : version.tag._id, - slug: o.get('slug') + tags: version.tags === null ? 'tags_not_found' : version.tags, + slugs: o.get('slugs'), + isProposed } }) ) }) + }).catch(err => () => { + console.log(err) + return ErrorController.handleServerError(err, req, res, []) }) + }).catch(err => () => { + console.log(err) + return ErrorController.handleServerError(err, req, res, []) }) } static async organization(req: express.Request, res: express.Response) { - Organization.find({ slug: req.params.slug }).then(data => { + Organization.find({ slugs: { '$in': req.params.slug } }).then(data => { if (data.length === 0) { return ErrorController.notFoundHandler()(req, res) } else { const org = data[0] let version = org.get('publishedVersion') + let lastPublished = org.get('publishedAt') + let isProposed = false if (req.query.version === 'proposed') { + isProposed = true + lastPublished = org.get('updatedAt') version = org.get('proposedVersion') - } else if (org.get('publishedAt') === undefined || org.get('publishedAt') === null) { + } else if (lastPublished === undefined || lastPublished === null) { return ErrorController.notFoundHandler()(req, res) } - if (version.contacts !== null && version.contacts !== undefined) { - if (typeof version.contacts.address === 'string') { + // if (lastPublished !== null) { + // lastPublished = lastPublished + // } + if (Utils.isUsable(version.contacts)) { + if (Utils.isStrUsable(version.contacts, 'address')) { version.contacts.address = version.contacts.address.split('\n') } - if (typeof version.contacts.phone === 'string') { + if (Utils.isStrUsable(version.contacts, 'phone')) { let phone = version.contacts.phone if (phone.indexOf('+33') === 0) { phone = '0' + phone.substr(3) @@ -83,10 +107,16 @@ export default class PublicController { } res.render('organization.twig', { layout: 'standalone', - data: version + data: version, + lastPublished: lastPublished.toLocaleDateString('fr-FR') + ' ' + lastPublished.toLocaleTimeString('fr-FR', { timezone: '+2' }).substr(0, 5), + isProposed, + id: org.get('_id') }) } - }).catch(_ => res.status(404).render('not-found.twig')) + }).catch(err => () => { + console.log(err) + return ErrorController.handleServerError(err, req, res, []) + }) } static async about(req: express.Request, res: express.Response) { diff --git a/src/models/Organization.ts b/src/models/Organization.ts index e4bc10e..348c486 100644 --- a/src/models/Organization.ts +++ b/src/models/Organization.ts @@ -74,14 +74,14 @@ const OrganizationVersion = { priceLabel: { type: String, required: true }, description: { type: String } }], - tag: [Tag] + tags: [{ type: String }] } const Organization = new Schema({ adminName: { type: String, required: true }, email: { type: String, required: true }, token: { type: String, required: true }, - slug: [{ type: String }], // aliases system + slugs: [{ type: String }], // aliases system validationState: { type: AllowedString, required: true, diff --git a/static/assets/home.css b/static/assets/home.css index 3aaea36..ae7f2ba 100644 --- a/static/assets/home.css +++ b/static/assets/home.css @@ -181,6 +181,8 @@ margin-bottom: 1em; box-shadow: 0 0 8px 0px rgba(0,0,0,0.1); transition: all 0.2s; + height: 12em; + overflow: hidden; } .card:hover { @@ -202,7 +204,7 @@ } .card-content { - /* width: 100%; */ + width: 100%; padding: 1.5em; display: flex; flex-direction: column; @@ -238,12 +240,14 @@ color: #34495E; margin: 0; line-height: 1.6em; + position: relative; } .card-link { - color: #0029FF; - opacity: 0.75; - text-align: right; + position: absolute; + right: .5em; + bottom: 0; + margin-bottom: -.5em; } @media (max-width: 1350px) { diff --git a/static/assets/js/home.js b/static/assets/js/home.js index b862ff7..6917ae0 100644 --- a/static/assets/js/home.js +++ b/static/assets/js/home.js @@ -10,11 +10,6 @@ let navContent = document.getElementById('nav-content') let mosaic = document.getElementById('mosaic') let mosaicHeader = document.getElementById('mosaic-header') -organizations = organizations.map(org => { - org.tag = tags.filter(t => t._id === org.tag)[0] - return org -}) - navEnabler.onclick = async () => { if (!navOpened) { // open the menu @@ -105,28 +100,36 @@ function renderCard(organization) { titleContainer.appendChild(title) let icon = createEl('card-icon') - icon.innerHTML = `` + if (Array.isArray(organization.tags) && organization.tags.length > 0) { + let tag = tags.filter(tag => organization.tags[0] === tag._id)[0] + icon.innerHTML = `` + } titleContainer.appendChild(icon) upperContent.appendChild(titleContainer) let description = createEl('card-description') description.textContent = organization.description - upperContent.appendChild(description) - let link = createEl('card-link') - let aTag = createEl(0, 'a') - aTag.href = "/association/" + organization.slug + //let link = createEl('card-link') + let aTag = createEl('card-link', 'a') + aTag.href = "/association/" + organization.slugs[organization.slugs.length - 1] + if (organization.isProposed) { + aTag.href += "?version=proposed" + } aTag.textContent = "En savoir plus" - link.appendChild(aTag) + description.appendChild(aTag) + + upperContent.appendChild(description) + //link.appendChild(aTag) content.appendChild(upperContent) - content.appendChild(link) + //content.appendChild(link) card.appendChild(content) card.onclick = () => { @@ -152,7 +155,7 @@ function enableTag(node) { if (!all) { tagId = node.attributes['data-tag-id'].value } - let data = organizations.filter(orga => orga.tag._id === tagId || all) + let data = organizations.filter(orga => orga.tags.filter(id => id === tagId).length > 0 || all) let cards = renderMosaic(data) if (currentCardContainer !== null) { mosaic.removeChild(currentCardContainer) diff --git a/static/assets/main.css b/static/assets/main.css index 6312487..9763087 100644 --- a/static/assets/main.css +++ b/static/assets/main.css @@ -23,6 +23,35 @@ a:hover { color: #2980b9; } +/* sticky footer */ +html, body { + height: 100%; +} +body { + display: flex; + flex-direction: column; +} +.up-footer { + flex: 1 0 auto; +} +.sticky-footer { + flex-shrink: 0; +} + +/* Porposed alert */ +.proposed-alert { + position: fixed; + z-index: 9999999; + left: 1em; + bottom: 2em; + padding: 1em; + font-weight: bold; + border-radius: 3px; + text-transform: uppercase; + border: 1px solid #c0392b; + background-color: #e74c3c; + color: white; +} @media (max-width: 1600px) { .container { diff --git a/static/assets/organization.css b/static/assets/organization.css index e423878..48aa34f 100644 --- a/static/assets/organization.css +++ b/static/assets/organization.css @@ -538,13 +538,26 @@ section { ********************************************************************************/ +.mentions { + display: flex; + justify-content: center; + width: 100%; + flex-direction: column; + text-align: center; + color: #d35400; + margin-top: 2em; + margin-bottom: .5em; +} +.mentions div { + margin-bottom: .5em; +} + .footer { width: 100%; display: grid; grid-template-columns: 50% 25% 25%; grid-template-rows: 1fr; height: 1em; - margin-top: 2em; } .footer div:nth-child(1) { @@ -559,6 +572,9 @@ section { background-color: rgb(253, 110, 11, .83); } +/** +RESPONSIVE +**/ @media (max-width: 1200px) { .schedule-category-days-container { diff --git a/views/base.twig b/views/base.twig index 19e57ab..ac13e88 100644 --- a/views/base.twig +++ b/views/base.twig @@ -21,6 +21,7 @@ Github: https://github.com/lefuturiste {% block head %}{% endblock %} +