Implementing analytics page
This commit is contained in:
parent
932bf3e6ae
commit
b77354ec47
@ -2228,9 +2228,8 @@ smSelect.innerHTML = `
|
||||
-ms-grid-columns: 1fr auto;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-areas: 'heading heading' '. .';
|
||||
padding: 0.4rem 0.8rem;
|
||||
padding: var(--padding,0.6rem 0.8rem);
|
||||
background: rgba(var(--text-color), 0.06);
|
||||
border: solid 1px rgba(var(--text-color), 0.2);
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
@ -2359,7 +2358,18 @@ customElements.define('sm-select', class extends HTMLElement {
|
||||
return this.getAttribute('value')
|
||||
}
|
||||
set value(val) {
|
||||
this.setAttribute('value', val)
|
||||
const selectedOption = this.availableOptions.find(option => option.getAttribute('value') === val)
|
||||
if (selectedOption) {
|
||||
this.setAttribute('value', val)
|
||||
this.selectedOptionText.textContent = `${this.label}${selectedOption.textContent}`;
|
||||
if (this.previousOption) {
|
||||
this.previousOption.classList.remove('check-selected')
|
||||
}
|
||||
selectedOption.classList.add('check-selected')
|
||||
this.previousOption = selectedOption
|
||||
} else {
|
||||
console.warn(`There is no option with ${val} as value`)
|
||||
}
|
||||
}
|
||||
|
||||
reset(fire = true) {
|
||||
@ -2435,13 +2445,7 @@ customElements.define('sm-select', class extends HTMLElement {
|
||||
handleOptionSelection(e) {
|
||||
if (this.previousOption !== document.activeElement) {
|
||||
this.value = document.activeElement.getAttribute('value')
|
||||
this.selectedOptionText.textContent = `${this.label}${document.activeElement.textContent}`;
|
||||
this.fireEvent()
|
||||
if (this.previousOption) {
|
||||
this.previousOption.classList.remove('check-selected')
|
||||
}
|
||||
document.activeElement.classList.add('check-selected')
|
||||
this.previousOption = document.activeElement
|
||||
}
|
||||
}
|
||||
handleClick(e) {
|
||||
@ -3627,6 +3631,7 @@ customElements.define('tags-input', class extends HTMLElement {
|
||||
|
||||
this.reset = this.reset.bind(this)
|
||||
this.handleInput = this.handleInput.bind(this)
|
||||
this.addTag = this.addTag.bind(this)
|
||||
this.handleKeydown = this.handleKeydown.bind(this)
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
this.removeTag = this.removeTag.bind(this)
|
||||
@ -3637,6 +3642,10 @@ customElements.define('tags-input', class extends HTMLElement {
|
||||
get value() {
|
||||
return [...this.tags].join()
|
||||
}
|
||||
set value(arr) {
|
||||
this.reset();
|
||||
[...new Set(arr)].forEach(tag => this.addTag(tag))
|
||||
}
|
||||
get isValid() {
|
||||
return this.tags.size
|
||||
}
|
||||
@ -3650,6 +3659,17 @@ customElements.define('tags-input', class extends HTMLElement {
|
||||
this.input.previousElementSibling.remove()
|
||||
}
|
||||
}
|
||||
addTag(tagValue) {
|
||||
const tag = document.createElement('span')
|
||||
tag.dataset.value = tagValue
|
||||
tag.className = 'tag'
|
||||
tag.innerHTML = `
|
||||
<span class="tag-text">${tagValue}</span>
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
|
||||
`
|
||||
this.input.before(tag)
|
||||
this.tags.add(tagValue)
|
||||
}
|
||||
handleInput(e) {
|
||||
const inputValueLength = e.target.value.trim().length
|
||||
e.target.setAttribute('size', inputValueLength ? inputValueLength : '3')
|
||||
@ -3682,17 +3702,8 @@ customElements.define('tags-input', class extends HTMLElement {
|
||||
duration: 300,
|
||||
easing: 'ease'
|
||||
})
|
||||
}
|
||||
else {
|
||||
const tag = document.createElement('span')
|
||||
tag.dataset.value = tagValue
|
||||
tag.className = 'tag'
|
||||
tag.innerHTML = `
|
||||
<span class="tag-text">${tagValue}</span>
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
|
||||
`
|
||||
this.input.before(tag)
|
||||
this.tags.add(tagValue)
|
||||
} else {
|
||||
this.addTag(tagValue)
|
||||
}
|
||||
e.target.value = ''
|
||||
e.target.setAttribute('size', '3')
|
||||
|
||||
33
css/main.css
33
css/main.css
@ -44,6 +44,9 @@ body[data-theme=dark] * {
|
||||
body[data-theme=dark] sm-popup::part(popup) {
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
body[data-theme=dark] ::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
p,
|
||||
strong {
|
||||
@ -68,6 +71,20 @@ a:not([class]):focus-visible {
|
||||
box-shadow: 0 0 0 0.1rem rgba(var(--text-color), 1) inset;
|
||||
}
|
||||
|
||||
input[type=datetime-local] {
|
||||
padding: 0.6rem 0.8rem;
|
||||
background-color: rgba(var(--text-color), 0.06);
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
input[type=datetime-local]:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.1rem var(--accent-color);
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
-webkit-user-select: none;
|
||||
@ -129,7 +146,7 @@ tags-input {
|
||||
}
|
||||
|
||||
sm-button {
|
||||
--padding: 0.5rem 0.8rem;
|
||||
--padding: 0.6rem 0.8rem;
|
||||
}
|
||||
sm-button[variant=primary] .icon {
|
||||
fill: rgba(var(--background-color), 1);
|
||||
@ -310,7 +327,7 @@ ul {
|
||||
.empty-state {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 1.5rem 1rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.observe-empty-state:empty {
|
||||
@ -789,7 +806,8 @@ footer {
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
#publishing_requests {
|
||||
#publishing_requests,
|
||||
#article_analytics {
|
||||
margin-top: 2rem;
|
||||
align-content: flex-start;
|
||||
}
|
||||
@ -817,6 +835,15 @@ footer {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.article-row {
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
}
|
||||
.article-row__published {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
#preview_popup h1,
|
||||
#article h1 {
|
||||
font-size: 1.4rem;
|
||||
|
||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
@ -47,6 +47,9 @@ body[data-theme="dark"] {
|
||||
sm-popup::part(popup) {
|
||||
background-color: var(--foreground-color);
|
||||
}
|
||||
::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
@ -71,6 +74,20 @@ a:not([class]) {
|
||||
}
|
||||
}
|
||||
|
||||
input[type="datetime-local"] {
|
||||
padding: 0.6rem 0.8rem;
|
||||
background-color: rgba(var(--text-color), 0.06);
|
||||
border: none;
|
||||
border-radius: 0.3rem;
|
||||
font-size: 0.9rem;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 0.1rem var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
user-select: none;
|
||||
@ -118,7 +135,7 @@ tags-input {
|
||||
--border-radius: 0.3rem;
|
||||
}
|
||||
sm-button {
|
||||
--padding: 0.5rem 0.8rem;
|
||||
--padding: 0.6rem 0.8rem;
|
||||
&[variant="primary"] {
|
||||
.icon {
|
||||
fill: rgba(var(--background-color), 1);
|
||||
@ -305,7 +322,7 @@ ul {
|
||||
.empty-state {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
padding: 1.5rem 1rem;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.observe-empty-state:empty {
|
||||
@ -760,7 +777,8 @@ footer {
|
||||
padding: 1.5rem 0;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
#publishing_requests {
|
||||
#publishing_requests,
|
||||
#article_analytics {
|
||||
margin-top: 2rem;
|
||||
align-content: flex-start;
|
||||
}
|
||||
@ -788,6 +806,15 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.article-row {
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
&__published {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
#preview_popup,
|
||||
#article {
|
||||
h1 {
|
||||
|
||||
472
index.html
472
index.html
@ -228,8 +228,8 @@
|
||||
<div id="news_categories_list" class="flex"></div>
|
||||
</section>
|
||||
<section>
|
||||
<h5>Featured</h5>
|
||||
<h3>A Wake Up Call for India - Analyzing the falling GDP</h3>
|
||||
<h5>Trending</h5>
|
||||
<div id="trending_article_container"></div>
|
||||
</section>
|
||||
<section id="lastest_articles_section">
|
||||
<div class="flex align-center space-between">
|
||||
@ -273,15 +273,20 @@
|
||||
</footer>
|
||||
</article>
|
||||
<article id="dashboard" class="page page-layout hide-completely">
|
||||
<div class="flex space-between align-center">
|
||||
<h2>Dashboard</h2>
|
||||
<strip-select>
|
||||
<strip-option value="requests" selected>Requests</strip-option>
|
||||
<strip-option value="analytics">Analytics</strip-option>
|
||||
<div class="flex justify-center align-center">
|
||||
<strip-select id="section_selector">
|
||||
<strip-option value="analytic" selected>Analytics</strip-option>
|
||||
<strip-option value="request">Requests</strip-option>
|
||||
</strip-select>
|
||||
</div>
|
||||
<ul id="publishing_requests" class="grid gap-1-5 observe-empty-state"></ul>
|
||||
<p class="empty-state">No requests</p>
|
||||
<section id="analytic_section" class="admin-section">
|
||||
<ul id="article_analytics" class="grid gap-1-5 observe-empty-state"></ul>
|
||||
<p class="empty-state">No articles</p>
|
||||
</section>
|
||||
<section id="request_section" class="admin-section hide-completely">
|
||||
<ul id="publishing_requests" class="grid gap-1-5 observe-empty-state"></ul>
|
||||
<p class="empty-state">No requests</p>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
<sm-popup id="preview_popup">
|
||||
@ -302,6 +307,54 @@
|
||||
</header>
|
||||
<section id="preview_container"></section>
|
||||
</sm-popup>
|
||||
<sm-popup id="edit_popup">
|
||||
<header slot="header" class="popup__header">
|
||||
<div class="flex align-center">
|
||||
<button class="popup__header__close" onclick="hidePopup()">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||||
fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path
|
||||
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<h4>
|
||||
Edit
|
||||
</h4>
|
||||
</header>
|
||||
<section class="grid gap-1-5">
|
||||
<div class="grid gap-0-5">
|
||||
<h5>Title</h5>
|
||||
<sm-input id="edit_title"></sm-input>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<h5>Summary</h5>
|
||||
<sm-textarea id="edit_summary" rows="4"></sm-textarea>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<h5>Select category</h5>
|
||||
<sm-select id="edit_category">
|
||||
<sm-option value="art">Art</sm-option>
|
||||
<sm-option value="culture">Culture</sm-option>
|
||||
<sm-option value="entertainment">Entertainment</sm-option>
|
||||
<sm-option value="politics">Politics</sm-option>
|
||||
<sm-option value="science">Science</sm-option>
|
||||
<sm-option value="sports">Sports</sm-option>
|
||||
<sm-option value="tech">Tech</sm-option>
|
||||
</sm-select>
|
||||
</div>
|
||||
<div class="grid gap-0-5">
|
||||
<h5>Add tags</h5>
|
||||
<tags-input id="edit_tags" limit="10"></tags-input>
|
||||
</div>
|
||||
<label class="grid gap-0-5">
|
||||
<h5>Publishing date</h5>
|
||||
<input type="datetime-local" id="edit_published">
|
||||
</label>
|
||||
<sm-button id="set_article_meta" variant="primary">Save</sm-button>
|
||||
</section>
|
||||
</sm-popup>
|
||||
<sm-popup id="user_popup">
|
||||
<header slot="header" class="popup__header">
|
||||
<div class="flex align-center">
|
||||
@ -335,6 +388,31 @@
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<template id="article_row_template">
|
||||
<li class="grid article-row">
|
||||
<div class="grid gap-0-5">
|
||||
<h4 class="article-card__title"></h4>
|
||||
<time class="article-row__published"></time>
|
||||
</div>
|
||||
<div class="flex align-center">
|
||||
<span class="article-row__votes"></span>
|
||||
<svg class="icon button__icon--right" xmlns="http://www.w3.org/2000/svg" height="24px"
|
||||
viewBox="0 0 24 24" width="24px" fill="#000000">
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path
|
||||
d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
</svg>
|
||||
</div>
|
||||
<button class="icon-only edit-article">
|
||||
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px"
|
||||
fill="#000000">
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path
|
||||
d="M14.06 9.02l.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
<template id="request_template">
|
||||
<li class="request-card">
|
||||
<h4 class="request-card__title"></h4>
|
||||
@ -661,7 +739,7 @@
|
||||
await Promise.all([
|
||||
floCloudAPI.requestObjectData('publishedVc'),
|
||||
floCloudAPI.requestGeneralData('publishing_requests', {
|
||||
callback: (d, e) => renderPublishingRequests(d)
|
||||
callback: (d, e) => renderDashboard(d)
|
||||
})
|
||||
])
|
||||
break;
|
||||
@ -778,14 +856,29 @@
|
||||
},
|
||||
requestCard(details) {
|
||||
const { message: { articleID, category, title }, time, vectorClock } = details
|
||||
if (floGlobals.appObjects.publishedVc[vectorClock]) return
|
||||
const clone = getRef('request_template').content.cloneNode(true).firstElementChild
|
||||
clone.dataset.vc = vectorClock
|
||||
clone.querySelector('.request-card__title').textContent = title
|
||||
clone.querySelector('.request-card__time').textContent = getFormattedTime(time)
|
||||
clone.querySelector('.publish-button').textContent = floGlobals.appObjects['articles'].hasOwnProperty(articleID) ? 'Update' : 'Publish'
|
||||
if (floGlobals.appObjects.publishedVc[vectorClock]) {
|
||||
// const requestTime = parseInt(vectorClock.split('_')[0])
|
||||
// if ((Date.now() - requestTime) / 1000 > 604800) {
|
||||
// delete floGlobals.appObjects.publishedVc[vectorClock]
|
||||
// }
|
||||
} else {
|
||||
const clone = getRef('request_template').content.cloneNode(true).firstElementChild
|
||||
clone.dataset.vc = vectorClock
|
||||
clone.querySelector('.request-card__title').textContent = title
|
||||
clone.querySelector('.request-card__time').textContent = getFormattedTime(time)
|
||||
clone.querySelector('.publish-button').textContent = floGlobals.appObjects['articles'].hasOwnProperty(articleID) ? 'Update' : 'Publish'
|
||||
return clone
|
||||
}
|
||||
},
|
||||
articleRow(articleID) {
|
||||
const { category, title, published, votes } = floGlobals.appObjects.articles[articleID]
|
||||
const clone = getRef('article_row_template').content.cloneNode(true).firstElementChild
|
||||
clone.querySelector('.article-card__title').textContent = title
|
||||
clone.querySelector('.article-row__published').textContent = relativeTime.from(published)
|
||||
clone.querySelector('.article-row__votes').textContent = votes
|
||||
clone.querySelector('.edit-article').dataset.articleId = articleID
|
||||
return clone
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const getArticles = async () => {
|
||||
@ -809,13 +902,9 @@
|
||||
}
|
||||
|
||||
function renderHomepage() {
|
||||
const sortedArticles = getArrayOfObj(floGlobals.appObjects.articles).sort((a, b) => b.published - a.published)
|
||||
const frag = document.createDocumentFragment()
|
||||
sortedArticles.slice(0, 5).forEach(articleDetail => frag.append(render.articleCard(articleDetail)))
|
||||
getRef('latest_articles_list').innerHTML = ''
|
||||
getRef('latest_articles_list').append(frag)
|
||||
|
||||
const categories = ['Art', 'Politics', 'Science', 'Tech']
|
||||
// Render article topics
|
||||
const categories = ['Art', 'Culture', 'Politics', 'Science', 'Tech']
|
||||
categories.forEach(category => frag.append(createElement('a', {
|
||||
textContent: category,
|
||||
attributes: { href: `#/explore?type=category&query=${category.toLowerCase()}` },
|
||||
@ -823,7 +912,31 @@
|
||||
})))
|
||||
getRef('news_categories_list').innerHTML = ''
|
||||
getRef('news_categories_list').append(frag)
|
||||
|
||||
// render latest articles
|
||||
const arrOfArticles = getArrayOfObj(floGlobals.appObjects.articles)
|
||||
const sortedArticles = arrOfArticles.sort((a, b) => b.published - a.published)
|
||||
sortedArticles.slice(0, 4).forEach(articleDetail => frag.append(render.articleCard(articleDetail)))
|
||||
getRef('latest_articles_list').innerHTML = ''
|
||||
getRef('latest_articles_list').append(frag)
|
||||
// Render trending article card
|
||||
let highestRatedArticle = arrOfArticles[0]
|
||||
for (let i = 1; i < arrOfArticles.length; i++) {
|
||||
if (highestRatedArticle.votes < arrOfArticles[i].votes) {
|
||||
highestRatedArticle = arrOfArticles[i]
|
||||
}
|
||||
}
|
||||
const { category, title, published, readTime, summary, uid } = highestRatedArticle
|
||||
getRef('trending_article_container').innerHTML = `
|
||||
<a href="#/explore?type=category&query=${category}" class="article-card__category">${category}</a>
|
||||
<a class="article-card--highlight grid" href="#/article?articleID=${uid}">
|
||||
<h3 id="trending_article__title">${title}</h3>
|
||||
<div class="flex">
|
||||
<span class="trending_article__read-time">${readTime} Min read</span>•<time
|
||||
class="trending_article__published">${relativeTime.from(published)}</time>
|
||||
</div>
|
||||
<p id="trending_article__summary">${summary}</p>
|
||||
</a>
|
||||
`
|
||||
}
|
||||
|
||||
const openedArticles = {}
|
||||
@ -843,17 +956,13 @@
|
||||
})
|
||||
getRef('article_contributors').innerHTML = ''
|
||||
getRef('article_contributors').append(frag)
|
||||
// implement live voting
|
||||
if (!openedArticles.hasOwnProperty(articleID)) {
|
||||
floCloudAPI.requestGeneralData(`article_${articleID}_votes`, {
|
||||
lowerVectorClock: floGlobals.appObjects.articles[articleID].lastCountedVC,
|
||||
lowerVectorClock: floGlobals.appObjects.articles[articleID].lastCountedVC + 1,
|
||||
callback: (allVotes, e) => {
|
||||
if (firstLoad) {
|
||||
let first = true
|
||||
for (const vote in allVotes) {
|
||||
if (first) {
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
floGlobals.appObjects.articles[articleID].votes += allVotes[vote].message.voteCount || 1
|
||||
}
|
||||
getRef('like_count').textContent = floGlobals.appObjects.articles[articleID].votes
|
||||
@ -883,65 +992,6 @@
|
||||
getRef('query_results_list').append(frag)
|
||||
}
|
||||
|
||||
async function renderPublishingRequests() {
|
||||
const requests = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`]
|
||||
const frag = document.createDocumentFragment()
|
||||
for (const key in requests) {
|
||||
const card = render.requestCard(requests[key])
|
||||
if (card)
|
||||
frag.prepend(card)
|
||||
}
|
||||
getRef('publishing_requests').innerHTML = ''
|
||||
getRef('publishing_requests').append(frag)
|
||||
}
|
||||
|
||||
function handleRequestClick(e) {
|
||||
if (e.target.closest('.publish-button')) {
|
||||
const button = e.target.closest('.publish-button');
|
||||
const vc = button.closest('.request-card').dataset.vc;
|
||||
const { message: { articleID, category, content, contributors, title, tags, readTime }, vectorClock } = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`][vc];
|
||||
const isPublished = floGlobals.appObjects['articles'].hasOwnProperty(articleID)
|
||||
getConfirmation(`${isPublished ? 'Update' : 'Publish'} article?`).then(res => {
|
||||
if (res) {
|
||||
floGlobals.appObjects['publishedVc'][vectorClock] = true
|
||||
floGlobals.appObjects.articlesContent[articleID] = content
|
||||
if (isPublished) {
|
||||
floGlobals.appObjects['articles'][articleID]['updated'] = Date.now();
|
||||
} else {
|
||||
floGlobals.appObjects.articles[articleID] = {
|
||||
published: Date.now(),
|
||||
votes: 0,
|
||||
lastCountedVC: ''
|
||||
}
|
||||
}
|
||||
floGlobals.appObjects['articles'][articleID].category = category
|
||||
floGlobals.appObjects['articles'][articleID].contributors = contributors
|
||||
floGlobals.appObjects['articles'][articleID].title = title
|
||||
floGlobals.appObjects['articles'][articleID].tags = tags
|
||||
floGlobals.appObjects['articles'][articleID].readTime = readTime
|
||||
Promise.all([
|
||||
floCloudAPI.updateObjectData('articles'),
|
||||
floCloudAPI.updateObjectData('publishedVc'),
|
||||
floCloudAPI.updateObjectData('articlesContent'),
|
||||
]).then(() => {
|
||||
notify(`${isPublished ? 'Updated' : 'Published'} article`, 'success')
|
||||
button.closest('.request-card').remove()
|
||||
})
|
||||
}
|
||||
})
|
||||
} else if (e.target.closest('.preview-button')) {
|
||||
const button = e.target.closest('.preview-button');
|
||||
const vc = button.closest('.request-card').dataset.vc;
|
||||
const { message: { content, title } } = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`][vc];
|
||||
|
||||
getRef('preview_container').innerHTML = DOMPurify.sanitize(content)
|
||||
getRef('preview_container').prepend(createElement('h1', {
|
||||
textContent: title
|
||||
}))
|
||||
showPopup('preview_popup')
|
||||
}
|
||||
}
|
||||
|
||||
let isSearchOn = false
|
||||
function toggleSearch() {
|
||||
const animOptions = {
|
||||
@ -1095,7 +1145,6 @@
|
||||
}
|
||||
})
|
||||
function animateLikeCount(voteCount = 1, articleID) {
|
||||
console.log('called')
|
||||
const animOptions = {
|
||||
fill: 'forwards',
|
||||
duration: 150,
|
||||
@ -1131,6 +1180,187 @@
|
||||
}
|
||||
}, 300))
|
||||
|
||||
async function renderDashboard() {
|
||||
const requests = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`]
|
||||
const frag = document.createDocumentFragment()
|
||||
for (const key in requests) {
|
||||
const card = render.requestCard(requests[key])
|
||||
if (card)
|
||||
frag.prepend(card)
|
||||
}
|
||||
floCloudAPI.updateObjectData('publishedVc')
|
||||
getRef('publishing_requests').innerHTML = ''
|
||||
getRef('publishing_requests').append(frag)
|
||||
|
||||
for (const articleKey in floGlobals.appObjects.articles) {
|
||||
const card = render.articleRow(articleKey)
|
||||
frag.prepend(card)
|
||||
}
|
||||
getRef('article_analytics').innerHTML = ''
|
||||
getRef('article_analytics').append(frag)
|
||||
}
|
||||
|
||||
function publishArticle(vc) {
|
||||
const { message: { articleID, content, contributors, title, readTime }, vectorClock } = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`][vc];
|
||||
const isPublished = floGlobals.appObjects['articles'].hasOwnProperty(articleID)
|
||||
getConfirmation(`${isPublished ? 'Update' : 'Publish'} article?`).then(res => {
|
||||
if (res) {
|
||||
const { title, category, summary, published, tags } = getArticleMeta()
|
||||
floGlobals.appObjects['publishedVc'][vectorClock] = true
|
||||
floGlobals.appObjects.articlesContent[articleID] = content
|
||||
if (isPublished) {
|
||||
floGlobals.appObjects['articles'][articleID]['updated'] = Date.now();
|
||||
} else {
|
||||
floGlobals.appObjects.articles[articleID] = {
|
||||
published: Date.now(),
|
||||
votes: 0,
|
||||
lastCountedVC: ''
|
||||
}
|
||||
floGlobals.appObjects.articleVotes[articleID] = { votes: {} }
|
||||
}
|
||||
floGlobals.appObjects['articles'][articleID].category = category
|
||||
floGlobals.appObjects['articles'][articleID].contributors = contributors
|
||||
floGlobals.appObjects['articles'][articleID].title = title
|
||||
floGlobals.appObjects['articles'][articleID].tags = tags
|
||||
floGlobals.appObjects['articles'][articleID].readTime = readTime
|
||||
floGlobals.appObjects['articles'][articleID].summary = summary
|
||||
Promise.all([
|
||||
floCloudAPI.updateObjectData('articles'),
|
||||
floCloudAPI.updateObjectData('publishedVc'),
|
||||
floCloudAPI.updateObjectData('articlesContent'),
|
||||
floCloudAPI.updateObjectData('articleVotes'),
|
||||
]).then(() => {
|
||||
notify(`${isPublished ? 'Updated' : 'Published'} article`, 'success')
|
||||
button.closest('.request-card').remove()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleRequestClick(e) {
|
||||
if (e.target.closest('.publish-button')) {
|
||||
const button = e.target.closest('.publish-button');
|
||||
const vc = button.closest('.request-card').dataset.vc;
|
||||
const { message: { articleID, title } } = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`][vc]
|
||||
const isPublished = floGlobals.appObjects['articles'].hasOwnProperty(articleID)
|
||||
setArticleMeta({ title })
|
||||
showPopup('edit_popup')
|
||||
} else if (e.target.closest('.preview-button')) {
|
||||
const button = e.target.closest('.preview-button');
|
||||
const vc = button.closest('.request-card').dataset.vc;
|
||||
const { message: { content, title } } = floGlobals.generalData[`publishing_requests|${floGlobals.adminID}|${floGlobals.application}`][vc];
|
||||
|
||||
getRef('preview_container').innerHTML = DOMPurify.sanitize(content)
|
||||
getRef('preview_container').prepend(createElement('h1', {
|
||||
textContent: title
|
||||
}))
|
||||
showPopup('preview_popup')
|
||||
}
|
||||
}
|
||||
|
||||
function setArticleMeta(details) {
|
||||
const { category, title, tags, summary, published } = details
|
||||
getRef('edit_title').value = title;
|
||||
getRef('edit_summary').value = summary || '';
|
||||
getRef('edit_category').value = category || '';
|
||||
getRef('edit_tags').value = tags || [];
|
||||
const now = Date.now()
|
||||
getRef('edit_published').value = new Date(published || now).toISOString().substr(0, new Date(published || now).toISOString().indexOf("."))
|
||||
}
|
||||
function getArticleMeta() {
|
||||
return {
|
||||
title: getRef('edit_title').value.trim(),
|
||||
category: getRef('edit_category').value,
|
||||
summary: getRef('edit_summary').value.trim(),
|
||||
published: getRef('edit_published').value,
|
||||
tags: getRef('edit_tags').value,
|
||||
}
|
||||
}
|
||||
function handleAnalyticsClick(e) {
|
||||
if (e.target.closest('.edit-article')) {
|
||||
const button = e.target.closest('.edit-article');
|
||||
const articleID = button.dataset.articleId;
|
||||
setArticleMeta(floGlobals.appObjects.articles[articleID])
|
||||
showPopup('edit_popup')
|
||||
}
|
||||
}
|
||||
|
||||
function updateArticle() {
|
||||
getConfirmation('Update article meta data?').then(res => {
|
||||
if (res) {
|
||||
const { title, category, summary, published, tags } = getArticleMeta()
|
||||
floGlobals.appObjects['articles'][articleID].category = category
|
||||
floGlobals.appObjects['articles'][articleID].title = title
|
||||
floGlobals.appObjects['articles'][articleID].tags = tags
|
||||
floGlobals.appObjects['articles'][articleID].summary = summary
|
||||
Promise.all([
|
||||
floCloudAPI.updateObjectData('articles'),
|
||||
floCloudAPI.updateObjectData('publishedVc'),
|
||||
]).then(() => {
|
||||
notify(`Updated article meta data`, 'success')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function calculateVotes() {
|
||||
await floCloudAPI.requestObjectData('articleVotes')
|
||||
const articlesVotesProm = []
|
||||
const articleIDs = []
|
||||
for (const articleKey in floGlobals.appObjects.articles) {
|
||||
if (floGlobals.appObjects.articleVotes.hasOwnProperty(articleKey) && floGlobals.appObjects.articles[articleKey].lastCountedVC !== '') {
|
||||
articlesVotesProm.push(
|
||||
floCloudAPI.requestGeneralData(`article_${articleKey}_votes`, {
|
||||
lowerVectorClock: floGlobals.appObjects.articles[articleKey].lastCountedVC + 1
|
||||
})
|
||||
)
|
||||
} else {
|
||||
articlesVotesProm.push(
|
||||
floCloudAPI.requestGeneralData(`article_${articleKey}_votes`)
|
||||
)
|
||||
}
|
||||
articleIDs.push(articleKey)
|
||||
}
|
||||
Promise.all(articlesVotesProm).then(res => {
|
||||
res.forEach((articleVotes, index) => {
|
||||
if (!floGlobals.appObjects.articleVotes.hasOwnProperty(articleIDs[index])) {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes = {}
|
||||
}
|
||||
if (floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC === '') {
|
||||
articleVotes = floGlobals.generalData[`article_${articleIDs[index]}_votes|${floGlobals.adminID}|${floGlobals.application}`]
|
||||
}
|
||||
for (const voteKey in articleVotes) {
|
||||
const { senderID, message: { voteCount }, type, vectorClock, time } = articleVotes[voteKey]
|
||||
const { votes } = floGlobals.appObjects.articleVotes[articleIDs[index]];
|
||||
if (votes[senderID]) {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes[senderID] += voteCount || 1
|
||||
} else {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes[senderID] = voteCount || 1
|
||||
}
|
||||
floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC = vectorClock
|
||||
}
|
||||
let totalArticleVotes = 0
|
||||
for (const voter in floGlobals.appObjects.articleVotes[articleIDs[index]].votes) {
|
||||
totalArticleVotes += floGlobals.appObjects.articleVotes[articleIDs[index]].votes[voter]
|
||||
}
|
||||
floGlobals.appObjects.articles[articleIDs[index]].votes = totalArticleVotes
|
||||
})
|
||||
Promise.all([
|
||||
floCloudAPI.updateObjectData('articles'),
|
||||
floCloudAPI.updateObjectData('articleVotes')
|
||||
])
|
||||
.then(() => {
|
||||
console.log('calculated votes')
|
||||
})
|
||||
})
|
||||
floGlobals.appObjects.articlesContent = await compactIDB.readData('appObjects', 'articlesContent')
|
||||
}
|
||||
|
||||
getRef('section_selector').addEventListener('change', e => {
|
||||
document.querySelectorAll('.admin-section').forEach(section => section.classList.add('hide-completely'))
|
||||
getRef(`${e.target.value}_section`).classList.remove('hide-completely')
|
||||
})
|
||||
|
||||
|
||||
function getSignedIn() {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -1167,68 +1397,12 @@
|
||||
floGlobals.isSubAdmin = floGlobals.subAdmins.includes(myFloID)
|
||||
if (floGlobals.isSubAdmin) {
|
||||
getRef('publishing_requests').addEventListener('click', handleRequestClick);
|
||||
getRef('article_analytics').addEventListener('click', handleAnalyticsClick);
|
||||
document.querySelectorAll('.admin-option').forEach(elem => elem.classList.remove('hide-completely'));
|
||||
await floCloudAPI.requestObjectData('articleVotes')
|
||||
const articlesVotesProm = []
|
||||
const articleIDs = []
|
||||
for (const articleKey in floGlobals.appObjects.articles) {
|
||||
if (floGlobals.appObjects.articleVotes[articleKey] & floGlobals.appObjects.articleVotes[articleKey].lastCountedVC !== '') {
|
||||
articlesVotesProm.push(
|
||||
floCloudAPI.requestGeneralData(`article_${articleKey}_votes`),
|
||||
{
|
||||
lowerVectorClock: floGlobals.appObjects.articleVotes[articleKey].lastCountedVC
|
||||
}
|
||||
)
|
||||
} else {
|
||||
articlesVotesProm.push(
|
||||
floCloudAPI.requestGeneralData(`article_${articleKey}_votes`)
|
||||
)
|
||||
}
|
||||
articleIDs.push(articleKey)
|
||||
}
|
||||
Promise.all(articlesVotesProm).then(res => {
|
||||
res.forEach((articleVotes, index) => {
|
||||
if (!floGlobals.appObjects.articleVotes.hasOwnProperty(articleIDs[index])) {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes = {}
|
||||
floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC = ''
|
||||
}
|
||||
let isFirst
|
||||
if (floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC === '') {
|
||||
articleVotes = floGlobals.generalData[`article_${articleIDs[index]}_votes|${floGlobals.adminID}|${floGlobals.application}`]
|
||||
} else {
|
||||
isFirst = true
|
||||
}
|
||||
for (const voteKey in articleVotes) {
|
||||
const { senderID, message: { voteCount }, type, vectorClock } = articleVotes[voteKey]
|
||||
if (isFirst && floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC === vectorClock) { // Skip over already counted VC
|
||||
isFirst = false
|
||||
continue
|
||||
}
|
||||
const { votes, lastCountedVC } = floGlobals.appObjects.articleVotes[articleIDs[index]];
|
||||
if (votes[senderID]) {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes[senderID] += voteCount || 1
|
||||
} else {
|
||||
floGlobals.appObjects.articleVotes[articleIDs[index]].votes[senderID] = voteCount || 1
|
||||
}
|
||||
floGlobals.appObjects.articles[articleIDs[index]].lastCountedVC = vectorClock
|
||||
}
|
||||
let totalArticleVotes = 0
|
||||
for (const voter in floGlobals.appObjects.articleVotes[articleIDs[index]].votes) {
|
||||
totalArticleVotes += floGlobals.appObjects.articleVotes[articleIDs[index]].votes[voter]
|
||||
}
|
||||
floGlobals.appObjects.articles[articleIDs[index]].votes = totalArticleVotes
|
||||
})
|
||||
Promise.all([
|
||||
floCloudAPI.updateObjectData('articles'),
|
||||
floCloudAPI.updateObjectData('articleVotes')
|
||||
])
|
||||
.then(() => {
|
||||
console.log('calculated votes')
|
||||
})
|
||||
})
|
||||
floGlobals.appObjects.articlesContent = await compactIDB.readData('appObjects', 'articlesContent')
|
||||
calculateVotes()
|
||||
} else {
|
||||
getRef('publishing_requests').removeEventListener('click', handleRequestClick)
|
||||
getRef('article_analytics').removeEventListener('click', handleAnalyticsClick);
|
||||
document.querySelectorAll('.admin-option').forEach(elem => elem.classList.add('hide-completely'))
|
||||
}
|
||||
if (location.hash.includes('sign_in') || location.hash.includes('sign_up'))
|
||||
|
||||
Loading…
Reference in New Issue
Block a user