From d534b7950ec091fd2465238c6e948a38a81ba32d Mon Sep 17 00:00:00 2001 From: rob Date: Sat, 11 Dec 2021 01:26:12 -0500 Subject: [PATCH] large changelist - Comment composer - Comment renderer - Comment create service - Display list and display engine enhancements - Added emoji picker for comments - Admin for site settings - added main-sidebar layout --- app/controllers/admin.js | 1 + app/controllers/admin/settings.js | 56 ++++++ app/controllers/post.js | 46 ++++- app/models/comment.js | 22 ++- app/services/comment.js | 173 ++++++++++++++++++ app/services/display-engine.js | 14 ++ app/services/gab-tv.js | 2 +- app/views/admin/components/menu.pug | 4 + app/views/admin/post/editor.pug | 10 +- app/views/admin/settings/editor.pug | 15 ++ .../comment/components/comment-standalone.pug | 3 + app/views/comment/components/comment.pug | 47 +++++ app/views/components/library.pug | 1 + app/views/components/navbar.pug | 10 +- app/views/components/page-sidebar.pug | 39 ++-- app/views/components/section-title.pug | 8 + app/views/index.pug | 69 +++---- app/views/layouts/main-sidebar.pug | 13 ++ app/views/layouts/main.pug | 3 +- app/views/page/view.pug | 27 +-- app/views/post/view.pug | 117 +++++++----- app/views/welcome/login.pug | 42 +++-- app/views/welcome/signup.pug | 7 +- client/js/site-app.js | 22 +++ client/less/site/image.less | 3 + lib/client/js/dtp-display-engine.js | 19 +- lib/site-platform.js | 5 +- package.json | 1 + yarn.lock | 122 +++++++++++- 29 files changed, 742 insertions(+), 159 deletions(-) create mode 100644 app/controllers/admin/settings.js create mode 100644 app/services/comment.js create mode 100644 app/views/admin/settings/editor.pug create mode 100644 app/views/comment/components/comment-standalone.pug create mode 100644 app/views/comment/components/comment.pug create mode 100644 app/views/components/section-title.pug create mode 100644 app/views/layouts/main-sidebar.pug diff --git a/app/controllers/admin.js b/app/controllers/admin.js index 315957f..77ee48a 100644 --- a/app/controllers/admin.js +++ b/app/controllers/admin.js @@ -49,6 +49,7 @@ class AdminController extends SiteController { router.use('/newsletter',await this.loadChild(path.join(__dirname, 'admin', 'newsletter'))); router.use('/page',await this.loadChild(path.join(__dirname, 'admin', 'page'))); router.use('/post',await this.loadChild(path.join(__dirname, 'admin', 'post'))); + router.use('/settings',await this.loadChild(path.join(__dirname, 'admin', 'settings'))); router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user'))); router.get('/', this.getHomeView.bind(this)); diff --git a/app/controllers/admin/settings.js b/app/controllers/admin/settings.js new file mode 100644 index 0000000..1f6994f --- /dev/null +++ b/app/controllers/admin/settings.js @@ -0,0 +1,56 @@ +// admin/settings.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const DTP_COMPONENT_NAME = 'admin:settings'; +const express = require('express'); + +const { SiteController } = require('../../../lib/site-lib'); + +class SettingsController extends SiteController { + + constructor (dtp) { + super(dtp, DTP_COMPONENT_NAME); + } + + async start ( ) { + const router = express.Router(); + router.use(async (req, res, next) => { + res.locals.currentView = 'admin'; + res.locals.adminView = 'settings'; + return next(); + }); + + router.post('/', this.postUpdateSettings.bind(this)); + + router.get('/', this.getSettingsView.bind(this)); + + return router; + } + + async postUpdateSettings (req, res, next) { + const { cache: cacheService } = this.dtp.services; + try { + const settingsKey = `settings:${this.dtp.config.site.domainKey}:site`; + await cacheService.setObject(settingsKey, req.body); + res.redirect('/admin/settings'); + } catch (error) { + return next(error); + } + } + + async getSettingsView (req, res, next) { + try { + res.render('admin/settings/editor'); + } catch (error) { + return next(error); + } + } +} + +module.exports = async (dtp) => { + let controller = new SettingsController(dtp); + return controller; +}; \ No newline at end of file diff --git a/app/controllers/post.js b/app/controllers/post.js index bc85bb3..a538d33 100644 --- a/app/controllers/post.js +++ b/app/controllers/post.js @@ -7,6 +7,7 @@ const DTP_COMPONENT_NAME = 'post'; const express = require('express'); +const multer = require('multer'); const { SiteController, SiteError } = require('../../lib/site-lib'); @@ -20,6 +21,8 @@ class PostController extends SiteController { const { dtp } = this; const { limiter: limiterService } = dtp.services; + const upload = multer({ dest: `/tmp/dtp-sites/${this.dtp.config.site.domainKey}`}); + const router = express.Router(); dtp.app.use('/post', router); @@ -31,6 +34,8 @@ class PostController extends SiteController { router.param('postSlug', this.populatePostSlug.bind(this)); + router.post('/:postSlug/comment', upload.none(), this.postComment.bind(this)); + router.get('/:postSlug', limiterService.create(limiterService.config.post.getView), this.getView.bind(this), @@ -56,10 +61,49 @@ class PostController extends SiteController { } } + async postComment (req, res) { + const { + comment: commentService, + displayEngine: displayEngineService, + } = this.dtp.services; + try { + const displayList = displayEngineService.createDisplayList('add-recipient'); + + res.locals.comment = await commentService.create(req.user, 'Post', res.locals.post, req.body); + + displayList.setInputValue('textarea#content', ''); + displayList.setTextContent('#comment-character-count', '0'); + + let viewModel = Object.assign({ }, req.app.locals); + viewModel = Object.assign(viewModel, res.locals); + + const html = await commentService.templates.comment(viewModel); + displayList.addElement('ul#post-comment-list', 'afterBegin', html); + + displayList.showNotification( + 'Comment created', + 'success', + 'bottom-center', + 4000, + ); + res.status(200).json({ success: true, displayList }); + } catch (error) { + res.status(error.statusCode || 500).json({ success: false, message: error.message }); + } + } + async getView (req, res, next) { - const { resource: resourceService } = this.dtp.services; + const { comment: commentService, resource: resourceService } = this.dtp.services; try { await resourceService.recordView(req, 'Post', res.locals.post._id); + + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.comments = await commentService.getForResource( + res.locals.post, + ['published', 'mod-warn'], + res.locals.pagination, + ); + res.render('post/view'); } catch (error) { this.log.error('failed to service post view', { postId: res.locals.post._id, error }); diff --git a/app/models/comment.js b/app/models/comment.js index 8917984..608692b 100644 --- a/app/models/comment.js +++ b/app/models/comment.js @@ -9,16 +9,36 @@ const path = require('path'); const mongoose = require('mongoose'); const Schema = mongoose.Schema; +const CommentHistorySchema = new Schema({ + created: { type: Date }, + content: { type: String, maxlength: 3000 }, +}); + const { CommentStats, CommentStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js')); +const COMMENT_STATUS_LIST = ['published', 'removed', 'mod-warn', 'mod-removed']; + const CommentSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: 1 }, resourceType: { type: String, enum: ['Post', 'Page', 'Newsletter'], required: true }, resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, - replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' }, author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, + replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' }, + status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true }, content: { type: String, required: true, maxlength: 3000 }, + contentHistory: { type: [CommentHistorySchema], select: false }, stats: { type: CommentStats, default: CommentStatsDefaults, required: true }, }); +/* + * An index to optimize finding replies to a specific comment + */ +CommentSchema.index({ + resource: 1, + replyTo: 1, +}, { + partialFilterExpression: { $exists: { replyTo: 1 } }, + name: 'comment_replies', +}); + module.exports = mongoose.model('Comment', CommentSchema); \ No newline at end of file diff --git a/app/services/comment.js b/app/services/comment.js new file mode 100644 index 0000000..175782c --- /dev/null +++ b/app/services/comment.js @@ -0,0 +1,173 @@ +// comment.js +// Copyright (C) 2021 Digital Telepresence, LLC +// License: Apache-2.0 + +'use strict'; + +const path = require('path'); + +const mongoose = require('mongoose'); + +const Comment = mongoose.model('Comment'); // jshint ignore:line + +const pug = require('pug'); +const striptags = require('striptags'); + +const { SiteService, SiteError } = require('../../lib/site-lib'); + +class CommentService extends SiteService { + + constructor (dtp) { + super(dtp, module.exports); + this.populateComment = [ + { + path: 'author', + select: '', + }, + { + path: 'replyTo', + }, + ]; + } + + async start ( ) { + this.templates = { }; + this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug')); + } + + async create (author, resourceType, resource, commentDefinition) { + const NOW = new Date(); + let comment = new Comment(); + + comment.created = NOW; + comment.resourceType = resourceType; + comment.resource = resource._id; + comment.author = author._id; + if (commentDefinition.replyTo) { + comment.replyTo = mongoose.Types.ObjectId(commentDefinition.replyTo); + } + if (commentDefinition.content) { + comment.content = striptags(commentDefinition.content.trim()); + } + + await comment.save(); + + const model = mongoose.model(resourceType); + await model.updateOne( + { _id: resource._id }, + { + $inc: { 'stats.commentCount': 1 }, + }, + ); + + /* + * increment the reply count of every parent comment until you reach a + * comment with no parent. + */ + + let replyTo = comment.replyTo; + while (replyTo) { + await Comment.updateOne( + { _id: replyTo }, + { + $inc: { 'stats.replyCount': 1 }, + }, + ); + let parent = await Comment.findById(replyTo).select('replyTo').lean(); + if (parent.replyTo) { + replyTo = parent.replyTo; + } else { + replyTo = false; + } + } + + comment = comment.toObject(); + comment.author = author; + return comment; + } + + async update (comment, commentDefinition) { + const updateOp = { $set: { } }; + + if (!commentDefinition.content || (commentDefinition.content.length === 0)) { + throw new SiteError(406, 'The comment cannot be empty'); + } + updateOp.$set.content = striptags(commentDefinition.content.trim()); + updateOp.$push = { + contentHistory: { + created: new Date(), + content: comment.content, + }, + }; + this.log.info('updating comment content', { commentId: comment._id }); + await Comment.updateOne({ _id: comment._id }, updateOp); + } + + async setStatus (comment, status) { + await Comment.updateOne({ _id: comment._id }, { $set: { status } }); + } + + /** + * Pushes the current comment content to the contentHistory array, sets the + * content field to 'Content removed' and updates the comment status to the + * status provided. This preserves the comment content, but removes it from + * public view. + * @param {Document} comment + * @param {String} status + */ + async remove (comment, status = 'removed') { + await Comment.updateOne( + { _id: comment._id }, + { + $set: { + status, + content: 'Comment removed', + }, + $push: { + contentHistory: { + created: new Date(), + content: comment.content, + }, + }, + }, + ); + } + + async getForResource (resource, statuses, pagination) { + const comments = await Comment + .find({ resource: resource._id, status: { $in: statuses } }) + .sort({ created: 1 }) + .skip(pagination.skip) + .limit(pagination.cpp) + .populate(this.populateComment) + .lean(); + return comments; + } + + async getContentHistory (comment, pagination) { + /* + * Extract a page from the contentHistory using $slice on the array + */ + const fullComment = await Comment + .findOne( + { _id: comment._id }, + { + contentHistory: { + $sort: { created: 1 }, + $slice: [pagination.skip, pagination.cpp], + }, + } + ) + .select('contentHistory').lean(); + if (!fullComment) { + throw new SiteError(404, 'Comment not found'); + } + return fullComment.contentHistory || [ ]; + } +} + +module.exports = { + slug: 'comment', + name: 'comment', + create: (dtp) => { return new CommentService(dtp); }, +}; diff --git a/app/services/display-engine.js b/app/services/display-engine.js index 3e3b971..77d322f 100644 --- a/app/services/display-engine.js +++ b/app/services/display-engine.js @@ -26,6 +26,13 @@ class DisplayList { }); } + showModal (html) { + this.commands.push({ + action: 'showModal', + params: { html }, + }); + } + addElement (selector, where, html) { this.commands.push({ selector, action: 'addElement', @@ -40,6 +47,13 @@ class DisplayList { }); } + setInputValue (selector, value) { + this.commands.push({ + selector, action: 'setInputValue', + params: { value }, + }); + } + replaceElement (selector, html) { this.commands.push({ selector, action: 'replaceElement', diff --git a/app/services/gab-tv.js b/app/services/gab-tv.js index 6be324f..9e10fa1 100644 --- a/app/services/gab-tv.js +++ b/app/services/gab-tv.js @@ -6,7 +6,7 @@ const fetch = require('node-fetch'); // jshint ignore:line -const CACHE_DURATION = 1000 * 60 * 5; +const CACHE_DURATION = 60 * 5; const { SiteService } = require('../../lib/site-lib'); diff --git a/app/views/admin/components/menu.pug b/app/views/admin/components/menu.pug index 562ae8a..4689df3 100644 --- a/app/views/admin/components/menu.pug +++ b/app/views/admin/components/menu.pug @@ -6,6 +6,10 @@ ul.uk-nav.uk-nav-default span.nav-item-icon i.fas.fa-home span.uk-margin-small-left Home + a(href="/admin/settings") + span.nav-item-icon + i.fas.fa-cog + span.uk-margin-small-left Settings li.uk-nav-divider diff --git a/app/views/admin/post/editor.pug b/app/views/admin/post/editor.pug index 4d8e0ff..50a413e 100644 --- a/app/views/admin/post/editor.pug +++ b/app/views/admin/post/editor.pug @@ -18,11 +18,13 @@ block content label(for="slug").uk-form-label URL slug - var postSlug; - postSlug = post.slug.split('-'); - postSlug.pop(); - postSlug = postSlug.join('-'); + if (post) { + postSlug = post.slug.split('-'); + postSlug.pop(); + postSlug = postSlug.join('-'); + } input(id="slug", name="slug", type="text", placeholder= "Enter post URL slug", value= post ? postSlug : undefined).uk-input - .uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post.slug || 'your-slug-here'} + .uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post ? post.slug : 'your-slug-here'} .uk-margin label(for="summary").uk-form-label Post summary textarea(id="summary", name="summary", rows="4", placeholder= "Enter post summary (text only, no HTML)").uk-textarea= post ? post.summary : undefined diff --git a/app/views/admin/settings/editor.pug b/app/views/admin/settings/editor.pug new file mode 100644 index 0000000..06cd950 --- /dev/null +++ b/app/views/admin/settings/editor.pug @@ -0,0 +1,15 @@ +extends ../layouts/main +block content + + form(method="POST", action="/admin/settings").uk-form + .uk-margin + label(for="name").uk-form-label Site name + input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input + .uk-margin + label(for="description").uk-form-label Site description + input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input + .uk-margin + label(for="company").uk-form-label Company name + input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).uk-input + + button(type="submit").uk-button.dtp-button-primary Save Settings \ No newline at end of file diff --git a/app/views/comment/components/comment-standalone.pug b/app/views/comment/components/comment-standalone.pug new file mode 100644 index 0000000..a5a1f34 --- /dev/null +++ b/app/views/comment/components/comment-standalone.pug @@ -0,0 +1,3 @@ +include ../../components/library +include comment ++renderComment(comment) \ No newline at end of file diff --git a/app/views/comment/components/comment.pug b/app/views/comment/components/comment.pug new file mode 100644 index 0000000..4f6a8e3 --- /dev/null +++ b/app/views/comment/components/comment.pug @@ -0,0 +1,47 @@ +mixin renderComment (comment) + .uk-card.uk-card-secondary.uk-card-small.uk-border-rounded + .uk-card-body + div(uk-grid).uk-grid-small + .uk-width-auto + img(src="/img/default-member.png").site-profile-picture.sb-small + .uk-width-expand + div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small + if comment.author.displayName + .uk-width-auto + span= comment.author.displayName + .uk-width-auto= comment.author.username + .uk-width-auto= moment(comment.created).fromNow() + div!= marked.parse(comment.content) + div(uk-grid).uk-grid-small.uk-text-small + .uk-width-auto + button( + type="button", + data-comment-id= comment._id, + onclick="return dtp.app.upvoteComment(event);", + title="Upvote this comment", + ).uk-button.uk-button-link + +renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) + .uk-width-auto + button( + type="button", + data-comment-id= comment._id, + onclick="return dtp.app.downvoteComment(event);", + title="Downvote this comment", + ).uk-button.uk-button-link + +renderButtonIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) + .uk-width-auto + button( + type="button", + data-comment-id= comment._id, + onclick="return dtp.app.openReplies(event);", + title="Load replies to this comment", + ).uk-button.uk-button-link + +renderButtonIcon('fa-comment', formatCount(comment.stats.replyCount)) + .uk-width-auto + button( + type="button", + data-comment-id= comment._id, + onclick="return dtp.app.openReplyComposer(event);", + title="Write a reply to this comment", + ).uk-button.uk-button-link + +renderButtonIcon('fa-reply', 'reply') \ No newline at end of file diff --git a/app/views/components/library.pug b/app/views/components/library.pug index 93557d3..9574a8f 100644 --- a/app/views/components/library.pug +++ b/app/views/components/library.pug @@ -1,6 +1,7 @@ //- common routines for all views everywhere include button-icon +include section-title - function formatCount(value) { diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index 55c93cc..601345a 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -26,11 +26,15 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top div.no-select if user.picture_url img( - src= user.picture_url || '/img/default-member.png', + src= user.picture_url, title="Member Menu", - ).profile-navbar + ).site-profile-picture.sb-navbar else - include missing-profile-icon + img( + src= "/img/default-member.png", + title="Member Menu", + ).site-profile-picture.sb-navbar + div(uk-dropdown={ mode: 'click' }).uk-navbar-dropdown ul.uk-nav.uk-navbar-dropdown-nav(style="z-index: 1024;") li.uk-nav-heading.uk-text-center= user.displayName || user.username diff --git a/app/views/components/page-sidebar.pug b/app/views/components/page-sidebar.pug index eba32cc..b799af1 100644 --- a/app/views/components/page-sidebar.pug +++ b/app/views/components/page-sidebar.pug @@ -13,29 +13,28 @@ mixin renderSidebarEpisode(episode) mixin renderPageSidebar ( ) //- Gab TV 3 Most Recent Episodes .uk-margin - .dtp-border-bottom - h3.uk-heading-bullet - a(href= gabTvChannel.home_page_url, target= "_blank", title= `${gabTvChannel.title} on Gab`).uk-link-reset Gab TV + +renderSectionTitle('Gab TV', { + label: 'Visit Channel', + title: gabTvChannel.title, + url: gabTvChannel.home_page_url, + }) + ul.uk-list each episode in gabTvChannel.items.slice(0, 3) li +renderSidebarEpisode(episode) //- Newsletter Signup - //- TODO Add sticky - .uk-margin - .dtp-border-bottom.uk-margin - h3.uk-heading-bullet Mailing List - - form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form - .uk-card.uk-card-secondary.uk-card-small - - .uk-card-body - p Join the #{site.name} FREE newsletter to get show updates in your inbox. - - .uk-margin - label(for="email").uk-form-label.sr-only Email Address - input(id="email", name="email", type="email", placeholder="johnsmith@example.com").uk-input - - .uk-card-footer - button(type="submit").uk-button.uk-button-primary Sign Up \ No newline at end of file + div(uk-sticky={ offset: 60, bottom: '#dtp-content-grid' }) + +renderSectionTitle('Mailing List') + .uk-margin + form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form + .uk-card.uk-card-secondary.uk-card-small + .uk-card-body + p Join the #{site.name} FREE newsletter to get show updates in your inbox. + .uk-margin + label(for="email").uk-form-label.sr-only Email Address + input(id="email", name="email", type="email", placeholder="johnsmith@example.com").uk-input + + .uk-card-footer + button(type="submit").uk-button.dtp-button-primary.uk-button-small Sign Up \ No newline at end of file diff --git a/app/views/components/section-title.pug b/app/views/components/section-title.pug new file mode 100644 index 0000000..99c8169 --- /dev/null +++ b/app/views/components/section-title.pug @@ -0,0 +1,8 @@ +mixin renderSectionTitle (title, barButton) + .dtp-border-bottom + div(uk-grid).uk-grid-small.uk-flex-middle + .uk-width-expand + h3.uk-heading-bullet.uk-margin-small= title + if barButton + .uk-width-auto + a(href= barButton.url, target= "_blank", title= barButton.title).uk-button.uk-button-link.uk-button-small= barButton.label \ No newline at end of file diff --git a/app/views/index.pug b/app/views/index.pug index 2cb1817..5dc5f4a 100644 --- a/app/views/index.pug +++ b/app/views/index.pug @@ -1,4 +1,4 @@ -extends layouts/main +extends layouts/main-sidebar block content include components/page-sidebar @@ -7,16 +7,16 @@ block content a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset div(uk-grid).uk-grid-small - div(class={ + div(class='uk-visible@m', class={ 'uk-flex-first': ((postIndex % postIndexModulus) === 0), 'uk-flex-last': ((postIndex % postIndexModulus) !== 0), }).uk-width-1-3 img(src="/img/default-poster.jpg").responsive - div(class={ + div(class='uk-width-1-1 uk-width-2-3@m', class={ 'uk-flex-first': ((postIndex % postIndexModulus) !== 0), 'uk-flex-last': ((postIndex % postIndexModulus) === 0), - }).uk-width-2-3 + }) article.uk-article h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title .uk-text-truncate= post.summary @@ -26,41 +26,28 @@ block content if post.updated .uk-width-auto updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")} - .uk-padding - .uk-container - //- Main Content Grid - div(uk-grid) - //- Main Content Column - div(class="uk-width-1-1 uk-width-2-3@m") - section.uk-section.uk-section-default.uk-padding-remove - .dtp-border-bottom - h3.uk-heading-bullet Featured - //- Featured Block - .uk-margin - div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%") - iframe( - src="https://tv.gab.com/channel/mrjoeprich/embed/what-is-just-joe-radio-61ad9b2165a83d20e95a465d", - width="960", - height="540", - style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;", - ) - //- Blog Posts - .uk-section.uk-section-default.uk-section-small - .dtp-border-bottom.uk-margin - h3.uk-heading-bullet Blog Posts - - if Array.isArray(posts) && (posts.length > 0) - - var postIndex = 1; - ul.uk-list.uk-list-divider.uk-list-small - each post in posts - li - +renderBlogPostListItem(post, postIndex, 2) - - postIndex += 1; - else - div There are no posts at this time. Please check back later! + +renderSectionTitle('Featured') + .uk-margin + div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%") + iframe( + src="https://tv.gab.com/channel/mrjoeprich/embed/what-is-just-joe-radio-61ad9b2165a83d20e95a465d", + width="960", + height="540", + style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;", + ) - //- pre= JSON.stringify(gabTvChannel, null, 2) - - //- Sidebar - div(class="uk-width-1-1 uk-width-1-3@m") - +renderPageSidebar() \ No newline at end of file + if featuredEmbed + div.dtp-featured-embed!= featuredEmbed + + //- Blog Posts + +renderSectionTitle('Blog Posts') + + if Array.isArray(posts) && (posts.length > 0) + - var postIndex = 1; + ul.uk-list.uk-list-divider.uk-list-small + each post in posts + li + +renderBlogPostListItem(post, postIndex, 2) + - postIndex += 1; + else + div There are no posts at this time. Please check back later! \ No newline at end of file diff --git a/app/views/layouts/main-sidebar.pug b/app/views/layouts/main-sidebar.pug new file mode 100644 index 0000000..55e474a --- /dev/null +++ b/app/views/layouts/main-sidebar.pug @@ -0,0 +1,13 @@ +extends main + +block content-container + section.uk-section.uk-section-default + .uk-container + div(uk-grid)#dtp-content-grid + div(class="uk-width-1-1 uk-width-2-3@m") + block content + div(class="uk-width-1-1 uk-width-1-3@m") + +renderPageSidebar() + + block page-footer + include ../components/page-footer \ No newline at end of file diff --git a/app/views/layouts/main.pug b/app/views/layouts/main.pug index e9776e0..4fef5b2 100644 --- a/app/views/layouts/main.pug +++ b/app/views/layouts/main.pug @@ -1,4 +1,5 @@ include ../components/library +include ../components/page-sidebar doctype html html(lang='en') head @@ -24,7 +25,7 @@ html(lang='en') block js script(src=`/uikit/js/uikit.min.js?v=${pkg.version}`) script(src=`/uikit/js/uikit-icons.min.js?v=${pkg.version}`) - script(src=`/fontawesome/js/fontawesome.min.js?v=${pkg.version}`) + //- script(src=`/fontawesome/js/fontawesome.min.js?v=${pkg.version}`) block pwa-support include ../components/pwa-support diff --git a/app/views/page/view.pug b/app/views/page/view.pug index f552498..f05f27b 100644 --- a/app/views/page/view.pug +++ b/app/views/page/view.pug @@ -1,22 +1,15 @@ -extends ../layouts/main +extends ../layouts/main-sidebar block content include ../components/page-sidebar - section.uk-section.uk-section-default.uk-section-small - .uk-container + article(dtp-page-id= page._id) + .uk-margin div(uk-grid) - .uk-width-2-3 - article(dtp-page-id= page._id) - .uk-margin - div(uk-grid) - .uk-width-expand - h1.article-title= page.title - if user && user.flags.isAdmin - .uk-width-auto - a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT - .uk-margin - != page.content - - .uk-width-1-3 - +renderPageSidebar() \ No newline at end of file + .uk-width-expand + h1.article-title= page.title + if user && user.flags.isAdmin + .uk-width-auto + a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT + .uk-margin + != page.content diff --git a/app/views/post/view.pug b/app/views/post/view.pug index 985b979..027ac66 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -1,54 +1,79 @@ -extends ../layouts/main +extends ../layouts/main-sidebar block content - include ../components/page-sidebar + include ../comment/components/comment - section.uk-section.uk-section-default.uk-section-small - .uk-container + article(dtp-post-id= post._id) + .uk-margin div(uk-grid) - .uk-width-2-3 - article(dtp-post-id= post._id) - .uk-margin - div(uk-grid) - .uk-width-expand - h1.article-title= post.title - if user && user.flags.isAdmin - .uk-width-auto - a(href=`/admin/post/${post._id}`).uk-button.dtp-button-text EDIT - .uk-text-lead= post.summary - .uk-margin - .uk-article-meta - div(uk-grid).uk-grid-small.uk-flex-top - .uk-width-expand - div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()} - .uk-width-auto - +renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) - .uk-width-auto - +renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount)) - .uk-width-auto - +renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount)) - .uk-width-auto - +renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount)) - .uk-margin - != post.content + .uk-width-expand + h1.article-title= post.title + .uk-text-lead= post.summary + .uk-margin + .uk-article-meta + div(uk-grid).uk-grid-small.uk-flex-top + .uk-width-expand + div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()} + if user && user.flags.isAdmin + .uk-width-auto + a(href=`/admin/post/${post._id}`) + +renderButtonIcon('fa-pen', 'edit') + .uk-width-auto + +renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) + .uk-width-auto + +renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount)) + .uk-width-auto + +renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount)) + .uk-width-auto + +renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount)) + .uk-margin + != post.content - if post.updated - .uk-margin - .uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. + if post.updated + .uk-margin + .uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. - if user && post.flags.enableComments - .dtp-border-bottom - h3.uk-heading-bullet Comments - - .uk-margin - form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event);").uk-form - .uk-margin - textarea(id="content", name="content", rows="4", placeholder="Enter comment").uk-textarea - .uk-text-small - div(uk-grid).uk-flex-between - .uk-width-auto You are commenting as: #{user.username} - .uk-width-auto 0 of 3000 + if user && post.flags.enableComments + +renderSectionTitle('Add a comment') + + .uk-margin + form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form + .uk-card.uk-card-secondary.uk-card-small + .uk-card-body + textarea( + id="content", + name="content", + rows="4", + maxlength="3000", + placeholder="Enter comment", + oninput="return dtp.app.onCommentInput(event);", + ).uk-textarea.uk-resize-vertical + .uk-text-small + div(uk-grid).uk-flex-between + .uk-width-auto You are commenting as: #{user.username} + .uk-width-auto #[span#comment-character-count 0] of 3,000 + .uk-card-footer + div(uk-grid).uk-flex-between + .uk-width-expand + button( + type="button", + data-target-element="content", + title="Add an emoji", + onclick="return dtp.app.showEmojiPicker(event);", + ).uk-button.dtp-button-default + span + i.far.fa-smile + .uk-width-auto button(type="submit").uk-button.dtp-button-primary Post comment + + .uk-margin + +renderSectionTitle('Comments') - .uk-width-1-3 - +renderPageSidebar() \ No newline at end of file + .uk-margin + if Array.isArray(comments) && (comments.length > 0) + ul#post-comment-list.uk-list + each comment in comments + +renderComment(comment) + else + ul#post-comment-list.uk-list + div There are no comments at this time. Please check back later. \ No newline at end of file diff --git a/app/views/welcome/login.pug b/app/views/welcome/login.pug index 39eaec0..bec2a15 100644 --- a/app/views/welcome/login.pug +++ b/app/views/welcome/login.pug @@ -4,21 +4,29 @@ block content form(method="POST", action="/auth/login").uk-form section.uk-section.uk-section-default .uk-container - fieldset.uk-fieldset - legend(class="uk-text-center uk-text-left@m").uk-legend Member Login - if loginResult - div(uk-alert).uk-alert.uk-alert-danger= loginResult - .uk-margin - label(for="username", class="uk-visible@m").uk-form-label Username or email address - input(id="username", name="username", type="text", placeholder="Enter username or email address").uk-input + .uk-card.uk-card-secondary.uk-card-small.uk-width-xlarge.uk-margin-auto.uk-border-rounded + .uk-card-header + +renderSectionTitle('Member Login') + .uk-card-body + fieldset.uk-fieldset + .uk-margin-small + div(uk-grid) + .uk-width-1-3 + img(src=`/img/icon/${site.domainKey}.png`).responsive + .uk-width-expand + if loginResult + div(uk-alert).uk-alert.uk-alert-danger= loginResult + .uk-margin + label(for="username", class="uk-visible@m").uk-form-label Username or email address + input(id="username", name="username", type="text", placeholder="Enter username or email address").uk-input - .uk-margin - label(for="password", class="uk-visible@m").uk-form-label Password - input(id="password", name="password", type="password", placeholder="Enter password").uk-input - .uk-text-muted.uk-text-small.uk-margin-small-top Remember that password we said you shouldn't forget? Type that here. - - section.uk-section.uk-section-secondary.uk-section-xsmall - .uk-container - .uk-margin - .uk-flex.uk-flex-center - button(type="submit").uk-button.dtp-button-primary Login \ No newline at end of file + .uk-margin + label(for="password", class="uk-visible@m").uk-form-label Password + input(id="password", name="password", type="password", placeholder="Enter password").uk-input + + .uk-card-footer + .uk-flex.uk-flex-right.uk-flex-middle + .uk-width-expand + a(href="/").uk-text-muted Forgot password + .uk-width-auto + button(type="submit").uk-button.dtp-button-primary Login \ No newline at end of file diff --git a/app/views/welcome/signup.pug b/app/views/welcome/signup.pug index dda7f25..8bbb1c3 100644 --- a/app/views/welcome/signup.pug +++ b/app/views/welcome/signup.pug @@ -4,7 +4,7 @@ block content form(method="POST", action="/user").uk-form section.uk-section.uk-section-muted.uk-section .uk-container.uk-container-small - p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here]. + p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here]. An account is required to comment on posts and use other site features. .uk-margin label(for="email").uk-form-label Email @@ -35,6 +35,11 @@ block content input(id="passwordv", name="passwordv", type="password", placeholder="Verify password").uk-input .uk-text-small.uk-text-muted.uk-margin-small-top(class="uk-visible@m") Please enter your password again to prove you're not an idiot. + .uk-margin + label + input(type="checkbox", checked).uk-checkbox + | Join #{site.name}'s Newsletter + section.uk-section.uk-section-secondary.uk-section .uk-container.uk-container-small .uk-margin-large diff --git a/client/js/site-app.js b/client/js/site-app.js index a195f66..72d1e68 100644 --- a/client/js/site-app.js +++ b/client/js/site-app.js @@ -13,6 +13,8 @@ import UIkit from 'uikit'; import QRCode from 'qrcode'; import Cropper from 'cropperjs'; +import { EmojiButton } from '@joeattardi/emoji-button'; + export default class DtpSiteApp extends DtpApp { constructor (user) { @@ -27,6 +29,10 @@ export default class DtpSiteApp extends DtpApp { input: document.querySelector('#chat-input-text'), isAtBottom: true, }; + + this.emojiPicker = new EmojiButton({ theme: 'dark' }); + this.emojiPicker.on('emoji', this.onEmojiSelected.bind(this)); + if (this.chat.messageList) { this.chat.messageList.addEventListener('scroll', this.onChatMessageListScroll.bind(this)); } @@ -472,6 +478,22 @@ export default class DtpSiteApp extends DtpApp { UIkit.modal.alert(`Failed to remove image: ${error.message}`); } } + + async onCommentInput (event) { + const label = document.getElementById('comment-character-count'); + label.textContent = numeral(event.target.value.length).format('0,0'); + } + + async showEmojiPicker (event) { + const targetElementName = (event.currentTarget || event.target).getAttribute('data-target-element'); + this.emojiTargetElement = document.getElementById(targetElementName); + + this.emojiPicker.togglePicker(this.emojiTargetElement); + } + + async onEmojiSelected (selection) { + this.emojiTargetElement.value += selection.emoji; + } } dtp.DtpSiteApp = DtpSiteApp; \ No newline at end of file diff --git a/client/less/site/image.less b/client/less/site/image.less index 4c811e9..3de1f83 100644 --- a/client/less/site/image.less +++ b/client/less/site/image.less @@ -54,6 +54,9 @@ img.site-profile-picture { max-width: 48px; border-radius: 6px; } + &.sb-navbar { + max-width: 48px; + } &.sb-small { max-width: 64px; } diff --git a/lib/client/js/dtp-display-engine.js b/lib/client/js/dtp-display-engine.js index cb58170..1be3f98 100644 --- a/lib/client/js/dtp-display-engine.js +++ b/lib/client/js/dtp-display-engine.js @@ -57,7 +57,7 @@ export default class DtpDisplayEngine { /* * action: addElement - * selector: Specifies the container element to insertAdjacentHtml + * selector: Specifies the container element to insertAdjacentHTML * where: 'beforebegin', 'afterbegin', 'beforeend', 'afterend' * html: the HTML content to insert at the container as specified */ @@ -67,7 +67,7 @@ export default class DtpDisplayEngine { console.debug('displayList.addElement has failed', { command }); return; } - container.insertAdjacentHtml(command.params.where, command.params.html); + container.insertAdjacentHTML(command.params.where, command.params.html); } async setTextContent (displayList, command) { @@ -81,6 +81,17 @@ export default class DtpDisplayEngine { }); } + async setInputValue (displayList, command) { + const elements = document.querySelectorAll(command.selector); + if (!elements || (elements.length === 0)) { + this.log.error('setInputValue', 'failed to find target elements', { command }); + return; + } + elements.forEach((element) => { + element.value = command.params.value; + }); + } + /* * action: replaceElement * selector: Specifies the element to be replaced @@ -196,4 +207,8 @@ export default class DtpDisplayEngine { async showNotification (displayList, command) { UIkit.notification(command.params); } + + async showModal (displayList, command) { + UIkit.modal.dialog(command.html); + } } \ No newline at end of file diff --git a/lib/site-platform.js b/lib/site-platform.js index c464a2c..8c496b0 100644 --- a/lib/site-platform.js +++ b/lib/site-platform.js @@ -166,7 +166,6 @@ module.exports.startWebServer = async (dtp) => { * Expose useful modules and information */ module.app.locals.DTP_SCRIPT_DEBUG = (process.env.NODE_ENV === 'local'); - module.app.locals.site = dtp.config.site; module.app.locals.pkg = require(path.join(dtp.config.root, 'package.json')); module.app.locals.moment = require('moment'); module.app.locals.numeral = require('numeral'); @@ -270,6 +269,7 @@ module.exports.startWebServer = async (dtp) => { * Application logic middleware */ module.app.use(async (req, res, next) => { + const { cache: cacheService } = dtp.services; try { res.locals.dtp = { request: req, @@ -291,6 +291,9 @@ module.exports.startWebServer = async (dtp) => { icon: 'fa-instagram' }, ]; + + const settingsKey = `settings:${dtp.config.site.domainKey}:site`; + res.locals.site = (await cacheService.getObject(settingsKey)) || dtp.config.site; return next(); } catch (error) { module.log.error('failed to populate general request data', { error }); diff --git a/package.json b/package.json index 28f3dee..45dabd2 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", + "@joeattardi/emoji-button": "^4.6.2", "@socket.io/redis-adapter": "^7.1.0", "anchorme": "^2.1.2", "argv": "^0.0.2", diff --git a/yarn.lock b/yarn.lock index 5a41567..484a3e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -885,11 +885,54 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@fortawesome/fontawesome-common-types@^0.2.36": + version "0.2.36" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903" + integrity sha512-a/7BiSgobHAgBWeN7N0w+lAhInrGxksn13uK7231n2m8EDPE3BMCl9NZLTGrj9ZXfCmC6LM0QLqXidIizVQ6yg== + "@fortawesome/fontawesome-free@^5.15.4": version "5.15.4" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== +"@fortawesome/fontawesome-svg-core@^1.2.28": + version "1.2.36" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.36.tgz#4f2ea6f778298e0c47c6524ce2e7fd58eb6930e3" + integrity sha512-YUcsLQKYb6DmaJjIHdDWpBIGCcyE/W+p/LMGvjQem55Mm2XWVAP5kWTMKWLv9lwpCVjpLxPyOMOyUocP1GxrtA== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.36" + +"@fortawesome/free-regular-svg-icons@^5.13.0": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz#b97edab436954333bbeac09cfc40c6a951081a02" + integrity sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.36" + +"@fortawesome/free-solid-svg-icons@^5.13.0": + version "5.15.4" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.15.4.tgz#2a68f3fc3ddda12e52645654142b9e4e8fbb6cc5" + integrity sha512-JLmQfz6tdtwxoihXLg6lT78BorrFyCf59SAwBM6qV/0zXyVeDygJVb3fk+j5Qat+Yvcxp1buLTY5iDh1ZSAQ8w== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.2.36" + +"@joeattardi/emoji-button@^4.6.2": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@joeattardi/emoji-button/-/emoji-button-4.6.2.tgz#75baf4ce27324e4d6fb90292f8b248235f638ad0" + integrity sha512-FhuzTmW3nVHLVp2BJfNX17CYV77fqtKZlx328D4h6Dw3cPTT1gJRNXN0jV7BvHgsl6Q/tN8DIQQxTUIO4jW3gQ== + dependencies: + "@fortawesome/fontawesome-svg-core" "^1.2.28" + "@fortawesome/free-regular-svg-icons" "^5.13.0" + "@fortawesome/free-solid-svg-icons" "^5.13.0" + "@popperjs/core" "^2.4.0" + "@types/twemoji" "^12.1.1" + escape-html "^1.0.3" + focus-trap "^5.1.0" + fuzzysort "^1.1.4" + tiny-emitter "^2.1.0" + tslib "^2.0.0" + twemoji "^13.0.0" + "@otplib/core@^12.0.1": version "12.0.1" resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d" @@ -928,6 +971,11 @@ "@otplib/plugin-crypto" "^12.0.1" "@otplib/plugin-thirty-two" "^12.0.1" +"@popperjs/core@^2.4.0": + version "2.11.0" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7" + integrity sha512-zrsUxjLOKAzdewIDRWy9nsV1GQsKBCWaGwsZQlCgr6/q+vjyZhFgqedLfFBuI9anTPEUT4APq9Mu0SZBTzIcGQ== + "@rollup/plugin-babel@^5.2.0": version "5.3.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz#9cb1c5146ddd6a4968ad96f209c50c62f92f9879" @@ -1065,6 +1113,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== +"@types/twemoji@^12.1.1": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@types/twemoji/-/twemoji-12.1.2.tgz#52578fd22665311e6a78d04f800275449d51c97e" + integrity sha512-3eMyKenMi0R1CeKzBYtk/Z2JIHsTMQrIrTah0q54o45pHTpWVNofU2oHx0jS8tqsDRhis2TbB6238WP9oh2l2w== + "@types/webidl-conversions@*": version "6.1.1" resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-6.1.1.tgz#e33bc8ea812a01f63f90481c666334844b12a09e" @@ -3139,7 +3192,7 @@ escape-goat@^2.0.0: resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== -escape-html@~1.0.3: +escape-html@^1.0.3, escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= @@ -3530,6 +3583,14 @@ flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" +focus-trap@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-5.1.0.tgz#64a0bfabd95c382103397dbc96bfef3a3cf8e5ad" + integrity sha512-CkB/nrO55069QAUjWFBpX6oc+9V90Qhgpe6fBWApzruMq5gnlh90Oo7iSSDK7pKiV5ugG6OY2AXM5mxcmL3lwQ== + dependencies: + tabbable "^4.0.0" + xtend "^4.0.1" + follow-redirects@^1.0.0, follow-redirects@^1.14.0: version "1.14.5" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.5.tgz#f09a5848981d3c772b5392309778523f8d85c381" @@ -3592,6 +3653,15 @@ fs-extra@3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" +fs-extra@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + fs-extra@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -3633,6 +3703,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +fuzzysort@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba" + integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -4895,6 +4970,22 @@ jsonfile@^3.0.0: optionalDependencies: graceful-fs "^4.1.6" +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922" + integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w== + dependencies: + universalify "^0.1.2" + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -7552,6 +7643,11 @@ systeminformation@^5.9.16: resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.9.16.tgz#097d5b585401b209b3448d1fe84551a5a582b904" integrity sha512-GDqen5wR9p3GVrTlyFYKbtQIE9eEhqd6Ya9Jr6HReSbDYJuYqhUgYTLuEt45qpSgNj1hKonUe/IzzdFXFmRBeg== +tabbable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-4.0.0.tgz#5bff1d1135df1482cf0f0206434f15eadbeb9261" + integrity sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ== + tapable@^2.1.1, tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -7667,6 +7763,11 @@ time-stamp@^1.0.0: resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= +tiny-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + tiny-inflate@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4" @@ -7789,7 +7890,7 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= -tslib@^2.3.0: +tslib@^2.0.0, tslib@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -7801,6 +7902,21 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +twemoji-parser@13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.1.0.tgz#65e7e449c59258791b22ac0b37077349127e3ea4" + integrity sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg== + +twemoji@^13.0.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-13.1.0.tgz#65bb71e966dae56f0d42c30176f04cbdae109913" + integrity sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew== + dependencies: + fs-extra "^8.0.1" + jsonfile "^5.0.0" + twemoji-parser "13.1.0" + universalify "^0.1.2" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -8644,7 +8760,7 @@ xmlhttprequest-ssl@~1.6.2: resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6" integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q== -xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==