Feature update

-- implemented voting for articles
-- implemented article votes calculation with sub admin
This commit is contained in:
sairaj mote 2022-01-17 15:37:21 +05:30
parent 737b08f5b4
commit d7ffee6a36
4 changed files with 395 additions and 42 deletions

View File

@ -27,7 +27,7 @@ body * {
--foreground-color: rgb(255, 255, 255);
--danger-color: rgb(255, 75, 75);
--green: #1cad59;
--yellow: #f3a600;
--like-color: #e91e63;
scrollbar-width: thin;
}
@ -40,7 +40,6 @@ body[data-theme=dark] * {
--foreground-color: rgb(24, 24, 24);
--danger-color: rgb(255, 106, 106);
--green: #00e676;
--yellow: #ffd13a;
}
body[data-theme=dark] sm-popup::part(popup) {
background-color: var(--foreground-color);
@ -614,20 +613,8 @@ main {
margin: 0 1rem;
z-index: 1;
background-color: rgba(var(--background-color), 1);
-webkit-clip-path: circle(0 at 1.5rem);
clip-path: circle(0 at 1.5rem);
transition: -webkit-clip-path 0.1s;
transition: clip-path 0.1s;
transition: clip-path 0.1s, -webkit-clip-path 0.1s;
}
#expanding_search.expanded {
transition: -webkit-clip-path 0.3s;
transition: clip-path 0.3s;
transition: clip-path 0.3s, -webkit-clip-path 0.3s;
-webkit-clip-path: circle(200%);
clip-path: circle(200%);
}
#expanding_search button {
#expanding_search .icon-only {
margin-right: 0.5rem;
}
@ -733,6 +720,67 @@ theme-toggle {
padding: 0.3rem 0.5rem;
}
.up-vote {
display: grid;
grid-template-columns: auto 1fr;
position: relative;
padding: 0.8rem;
border-radius: 2rem;
background-color: var(--foreground-color);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
border: solid rgba(var(--text-color), 0.2) thin;
}
.up-vote > * {
pointer-events: none;
}
.up-vote:active {
transform: none;
}
.up-vote:active .icon {
transform: scale(0.7);
}
.up-vote.liked {
background-color: var(--like-color);
color: #fff;
}
.up-vote.liked .icon {
fill: #fff;
}
.up-vote .expanding-heart,
.up-vote .ring {
grid-area: 1/1;
}
.up-vote .icon {
grid-area: 1/1;
fill: var(--like-color);
height: 1.5rem;
width: 1.5rem;
transition: transform 0.2s;
}
.ring {
border: 0.1rem solid var(--like-color);
border-radius: 50%;
height: 0.5rem;
width: 0.5rem;
justify-self: center;
}
.temp-count,
#like_count {
grid-area: 1/2;
}
.temp-count:not(:empty),
#like_count:not(:empty) {
margin-left: 0.4rem;
}
footer {
padding: 3rem 1.5rem;
justify-items: center;
}
#dashboard {
height: -webkit-max-content;
height: -moz-max-content;

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -24,7 +24,8 @@ body {
--foreground-color: rgb(255, 255, 255);
--danger-color: rgb(255, 75, 75);
--green: #1cad59;
--yellow: #f3a600;
--like-color: #e91e63;
scrollbar-width: thin;
}
@ -42,7 +43,6 @@ body[data-theme="dark"] {
--foreground-color: rgb(24, 24, 24);
--danger-color: rgb(255, 106, 106);
--green: #00e676;
--yellow: #ffd13a;
}
sm-popup::part(popup) {
background-color: var(--foreground-color);
@ -581,13 +581,7 @@ main {
margin: 0 1rem;
z-index: 1;
background-color: rgba(var(--background-color), 1);
clip-path: circle(0 at 1.5rem);
transition: clip-path 0.1s;
&.expanded {
transition: clip-path 0.3s;
clip-path: circle(200%);
}
button {
.icon-only {
margin-right: 0.5rem;
}
}
@ -691,6 +685,75 @@ theme-toggle {
border-radius: 0.3rem;
padding: 0.3rem 0.5rem;
}
.up-vote {
display: grid;
grid-template-columns: auto 1fr;
position: relative;
padding: 0.8rem;
border-radius: 2rem;
background-color: var(--foreground-color);
-webkit-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.1);
border: solid rgba(var(--text-color), 0.2) thin;
& > * {
pointer-events: none;
}
&:active {
-webkit-transform: none;
transform: none;
.icon {
-webkit-transform: scale(0.7);
transform: scale(0.7);
}
}
&.liked {
background-color: var(--like-color);
color: #fff;
.icon {
fill: #fff;
}
}
.expanding-heart,
.ring {
grid-area: 1/1;
}
& .icon {
grid-area: 1/1;
fill: var(--like-color);
height: 1.5rem;
width: 1.5rem;
-webkit-transition: -webkit-transform 0.2s;
transition: -webkit-transform 0.2s;
transition: transform 0.2s;
transition: transform 0.2s, -webkit-transform 0.2s;
}
}
.ring {
border: 0.1rem solid var(--like-color);
border-radius: 50%;
height: 0.5rem;
width: 0.5rem;
justify-self: center;
}
.temp-count,
#like_count {
grid-area: 1/2;
}
.temp-count:not(:empty),
#like_count:not(:empty) {
margin-left: 0.4rem;
}
footer {
padding: 3rem 1.5rem;
justify-items: center;
}
#dashboard {
height: max-content;

View File

@ -183,7 +183,7 @@
d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</button>
<div id="expanding_search">
<div id="expanding_search" class="hide-completely">
<div class="flex align-center">
<button class="icon-only" onclick="toggleSearch()">
<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24"
@ -192,14 +192,16 @@
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" />
</svg>
</button>
<sm-input id="search_articles" placeholder="Search articles" type="search">
<svg class="icon" slot="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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</sm-input>
<div class="w-100">
<sm-input id="search_articles" placeholder="Search articles" type="search">
<svg class="icon" slot="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="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" />
</svg>
</sm-input>
</div>
</div>
<div id="search_suggestions" class="flex direction-column hide-completely"></div>
</div>
@ -240,7 +242,8 @@
<article id="explore" class="page page-layout hide-completely">
<section class="flex direction-column">
<h1 id="explore_heading">Explore</h1>
<div id="query_results_list" class="grid"></div>
<div id="query_results_list" class="grid observe-empty-state"></div>
<p class="empty-state">No related articles</p>
</section>
</article>
<article id="article" class="page page-layout hide-completely">
@ -256,6 +259,18 @@
<div id="article_contributors" class="flex"></div>
<span>created with RanchiMall Content collaboration app</span>
</section>
<footer class="grid gap-1-5">
<h4>Loved the article? Don't forget leave a like.</h4>
<button id="upvote_button" class="button up-vote">
<svg class="icon" 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 id="like_count">Loading...</div>
</button>
</footer>
</article>
<article id="dashboard" class="page page-layout hide-completely">
<div class="flex space-between align-center">
@ -810,7 +825,7 @@
getRef('news_categories_list').append(frag)
}
async function renderArticle(articleID) {
async function renderArticle(articleID, firstLoad = true) {
const allArticles = await compactIDB.readData('appObjects', 'articlesContent')
const { title, published, readTime, contributors } = floGlobals.appObjects.articles[articleID]
getRef('article_title').textContent = title
@ -826,12 +841,35 @@
})
getRef('article_contributors').innerHTML = ''
getRef('article_contributors').append(frag)
totalVotes = floGlobals.appObjects.articles[articleID].votes ? floGlobals.appObjects.articles[articleID].votes : 0
floCloudAPI.requestGeneralData(`article_${articleID}_votes`, {
lowerVectorClock: floGlobals.appObjects.articles[articleID].lastCountedVC,
callback: (allVotes, e) => {
if (firstLoad) {
let first = true
for (const vote in allVotes) {
if (first) {
first = false
continue
}
totalVotes += allVotes[vote].message.voteCount || 1
}
getRef('like_count').textContent = getRelativeCount(totalVotes)
} else {
for (const msg in allVotes) {
if (typeof myFloID === 'undefined' || allVotes[msg].senderID !== myFloID)
animateLikeCount(allVotes[msg].message.voteCount)
}
}
firstLoad = false
}
})
}
function renderExplorePage(params) {
const { type, query } = params
const frag = document.createDocumentFragment()
const options = (type === 'category') ? { keys: ['category'] } : { keys: ['title', 'category', 'tags'], threshold: 0.3 }
const options = (type === 'category') ? { keys: ['category'], threshold: 0 } : { keys: ['title', 'category', 'tags'], threshold: 0.3 }
const fuse = new Fuse(getArrayOfObj(floGlobals.appObjects.articles).sort((a, b) => b.published - a.published), options)
const searchResult = fuse.search(query).map(v => v.item)
searchResult.forEach(articleDetail => frag.append(render.articleCard(articleDetail)))
@ -865,8 +903,11 @@
if (isPublished) {
floGlobals.appObjects['articles'][articleID]['updated'] = Date.now();
} else {
floGlobals.appObjects.articles[articleID] = {}
floGlobals.appObjects['articles'][articleID]['published'] = Date.now();
floGlobals.appObjects.articles[articleID] = {
published: Date.now(),
votes: 0,
lastCountedVC: ''
}
}
floGlobals.appObjects['articles'][articleID].category = category
floGlobals.appObjects['articles'][articleID].contributors = contributors
@ -896,11 +937,63 @@
}
}
let isSearchOn = false
function toggleSearch() {
if (!getRef('expanding_search').classList.contains('expanded')) {
getRef('search_articles').focusIn()
const animOptions = {
easing: 'ease',
fill: 'forwards'
}
if (!isSearchOn) {
getRef('expanding_search').classList.remove('hide-completely')
getRef('search_articles').focusIn()
animateTo(getRef('expanding_search').children[0].children[0], [
{ opacity: 0 },
{ opacity: 1 },
], {
...animOptions,
duration: 150
})
animateTo(getRef('search_articles').parentNode, [
{
transform: 'translateX(-2.8rem)',
opacity: 1
},
{
transform: 'none',
opacity: 1
},
], {
...animOptions,
duration: 150
})
isSearchOn = true
} else {
animateTo(getRef('expanding_search').children[0].children[0], [
{ opacity: 1 },
{ opacity: 0 },
], {
...animOptions,
duration: 100
})
animateTo(getRef('search_articles').parentNode, [
{
transform: 'none',
opacity: 1
},
{
transform: 'translateX(-2.8rem)',
opacity: 0
},
], {
...animOptions,
duration: 100
})
.onfinish = () => {
getRef('expanding_search').classList.add('hide-completely');
getRef('search_articles').value = ''
isSearchOn = false
}
}
getRef('expanding_search').classList.toggle('expanded')
}
getRef('search_articles').addEventListener('input', debounce((e) => {
const searchKey = e.target.value.trim()
@ -946,9 +1039,100 @@
if (e.target.value.trim() !== '' && e.code === 'Enter') {
location.hash = `#/explore?type=search&query=${e.target.value.trim()}`
e.target.value = ''
toggleSearch()
}
})
const slideInUp = [
{
opacity: 0,
transform: 'translateY(1rem)'
},
{
opacity: 1,
transform: 'translateY(0)'
},
]
const slideOutUp = [
{
opacity: 1,
transform: 'translateY(0)'
},
{
opacity: 0,
transform: 'translateY(-1rem)'
},
]
let tempVoteCount = 0
getRef('upvote_button').addEventListener('mouseup', function () {
if (myFloID) {
animateLikeCount(1)
tempVoteCount++;
const animOptions = {
fill: 'forwards',
duration: 300,
ease: 'easing',
}
const ring = document.createElement('div')
ring.classList.add('ring')
ring.animate([
{ transform: 'none' },
{ transform: 'scale(6)', opacity: 0 }
], animOptions).onfinish = e => {
e.target.cancel()
ring.remove()
}
this.append(ring)
this.firstElementChild.animate([
{ transform: 'scale(0.5)' },
{ transform: 'scale(1.4)', offset: 0.5 },
{ transform: 'none' },
], animOptions).onfinish = e => e.target.cancel()
}
})
function animateLikeCount(voteCount = 1) {
const animOptions = {
fill: 'forwards',
duration: 150,
ease: 'easing',
}
totalVotes += voteCount
getRef('like_count').animate(slideOutUp, animOptions)
.onfinish = (e) => {
e.target.cancel()
}
const tempCount = document.createElement('div')
tempCount.classList.add('temp-count')
tempCount.textContent = getRelativeCount(totalVotes)
getRef('like_count').after(tempCount)
tempCount.animate(slideInUp, animOptions)
.onfinish = () => {
getRef('like_count').textContent = getRelativeCount(totalVotes)
tempCount.remove()
}
}
getRef('upvote_button').addEventListener('click', debounce(() => {
if (myFloID) {
floCloudAPI.sendGeneralData({
voteCount: tempVoteCount,
}, `article_${pagesData.params.articleID}_votes`)
.then(res => {
tempVoteCount = 0
console.log('up voted')
})
.catch(err => console.log(err))
} else {
showPopup('sign_in_popup')
}
}, 300))
function getRelativeCount(count) {
if (count < 1000)
return count
else if (count < 1000000)
return parseFloat((count / 1000).toFixed(1)) + 'K'
else if (count < 1000000000)
return parseFloat((count / 1000000).toFixed(1)) + 'M'
}
function getSignedIn() {
@ -987,6 +1171,64 @@
if (floGlobals.isSubAdmin) {
getRef('publishing_requests').addEventListener('click', handleRequestClick);
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')
} else {
getRef('publishing_requests').removeEventListener('click', handleRequestClick)